Compare commits
	
		
			8 Commits
		
	
	
		
			90d7e64db3
			...
			fd19181eff
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fd19181eff | |||
| e45d1af857 | |||
| 30b3efa5fc | |||
| 2e648ac4fe | |||
| e05319ec3c | |||
| 2ae385b941 | |||
| 94640a70fa | |||
| 3f37ed95ed | 
							
								
								
									
										0
									
								
								logs_collector/account/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										3
									
								
								logs_collector/account/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,3 @@ | |||||||
|  | from django.contrib import admin | ||||||
|  | 
 | ||||||
|  | # Register your models here. | ||||||
							
								
								
									
										7
									
								
								logs_collector/account/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,7 @@ | |||||||
|  | from django.apps import AppConfig | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class AccountConfig(AppConfig): | ||||||
|  |     default_auto_field = 'django.db.models.BigAutoField' | ||||||
|  |     name = 'account' | ||||||
|  |     verbose_name = 'Auth and account management' | ||||||
							
								
								
									
										0
									
								
								logs_collector/account/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										3
									
								
								logs_collector/account/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,3 @@ | |||||||
|  | from django.db import models | ||||||
|  | 
 | ||||||
|  | # Create your models here. | ||||||
							
								
								
									
										3
									
								
								logs_collector/account/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,3 @@ | |||||||
|  | from django.test import TestCase | ||||||
|  | 
 | ||||||
|  | # Create your tests here. | ||||||
							
								
								
									
										40
									
								
								logs_collector/account/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,40 @@ | |||||||
|  | from django.conf import settings | ||||||
|  | from django.urls import path | ||||||
|  | from django.contrib.auth.views import LogoutView | ||||||
|  | 
 | ||||||
|  | from rest_framework_simplejwt.views import ( | ||||||
|  |     TokenObtainPairView, | ||||||
|  |     TokenRefreshView, | ||||||
|  |     TokenVerifyView | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | app_name = 'account' | ||||||
|  | 
 | ||||||
|  | urlpatterns = [ | ||||||
|  |     # WEB LOGOUT: | ||||||
|  |     path( | ||||||
|  |         'account/logout/', | ||||||
|  |         LogoutView.as_view(next_page=settings.LOGOUT_REDIRECT_URL), | ||||||
|  |         name='logout' | ||||||
|  |     ) | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | urlpatterns += [ | ||||||
|  |     # JWT AUTH: | ||||||
|  |     path( | ||||||
|  |         'api/v1/auth/token/', | ||||||
|  |         TokenObtainPairView.as_view(), | ||||||
|  |         name='token_obtain_pair' | ||||||
|  |     ), | ||||||
|  |     path( | ||||||
|  |         'api/v1/auth/token/refresh/', | ||||||
|  |         TokenRefreshView.as_view(), | ||||||
|  |         name='token_refresh' | ||||||
|  |     ), | ||||||
|  |     path( | ||||||
|  |         'api/v1/auth/token/verify/', | ||||||
|  |         TokenVerifyView.as_view(), | ||||||
|  |         name='token_verify' | ||||||
|  |     ), | ||||||
|  | ] | ||||||
							
								
								
									
										46
									
								
								logs_collector/account/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,46 @@ | |||||||
|  | from django.conf import settings | ||||||
|  | from django.contrib.auth import REDIRECT_FIELD_NAME | ||||||
|  | from django.contrib.auth.views import redirect_to_login | ||||||
|  | from django.http import HttpResponseRedirect | ||||||
|  | from django.shortcuts import resolve_url | ||||||
|  | from django.urls import reverse | ||||||
|  | from django.utils.http import url_has_allowed_host_and_scheme  # renamed Dj^3.* | ||||||
|  | from two_factor.admin import AdminSiteOTPRequired, AdminSiteOTPRequiredMixin | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # https://stackoverflow.com/questions/48600737/django-two-factor-auth-cant-access-admin-site | ||||||
|  | class AdminSiteOTPRequiredMixinRedirectSetup(AdminSiteOTPRequired): | ||||||
|  |     """ | ||||||
|  |     Fixes the current implementation of django-two-factor-auth = 1.15.3 | ||||||
|  |     when admin page is patched for 2fa | ||||||
|  |     (circular redirect - super user created with manage.py | ||||||
|  |     and cannot log in because he does not have a device configured). | ||||||
|  |     The class redirects to the setup page. | ||||||
|  |     After that, you can log in as usual. | ||||||
|  |     """ | ||||||
|  |     def login(self, request, extra_context=None): | ||||||
|  |         redirect_to = request.POST.get( | ||||||
|  |             REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME) | ||||||
|  |         ) | ||||||
|  |         # For users not yet verified the AdminSiteOTPRequired.has_permission | ||||||
|  |         # will fail. So use the standard admin has_permission check: | ||||||
|  |         # (is_active and is_staff) and then check for verification. | ||||||
|  |         # Go to index if they pass, otherwise make them setup OTP device. | ||||||
|  |         if request.method == "GET" and super( | ||||||
|  |             AdminSiteOTPRequiredMixin, self | ||||||
|  |         ).has_permission(request): | ||||||
|  |             # Already logged-in and verified by OTP | ||||||
|  |             if request.user.is_verified(): | ||||||
|  |                 # User has permission | ||||||
|  |                 index_path = reverse("admin:index", current_app=self.name) | ||||||
|  |             else: | ||||||
|  |                 # User has permission but no OTP set: | ||||||
|  |                 index_path = reverse("two_factor:setup", current_app=self.name) | ||||||
|  |             return HttpResponseRedirect(index_path) | ||||||
|  | 
 | ||||||
|  |         if not redirect_to or not url_has_allowed_host_and_scheme( | ||||||
|  |             url=redirect_to, allowed_hosts=[request.get_host()] | ||||||
|  |         ): | ||||||
|  |             redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL) | ||||||
|  | 
 | ||||||
|  |         return redirect_to_login(redirect_to) | ||||||
							
								
								
									
										0
									
								
								logs_collector/account/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										0
									
								
								logs_collector/collector/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										39
									
								
								logs_collector/collector/api/filters.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,39 @@ | |||||||
|  | from django_filters.rest_framework import ( | ||||||
|  |     CharFilter, | ||||||
|  |     FilterSet, | ||||||
|  |     NumberFilter, | ||||||
|  | ) | ||||||
|  | from django_filters import widgets | ||||||
|  | 
 | ||||||
|  | from collector.models import Archive, Ticket | ||||||
|  | from .utils import DateTimeFilterMixin | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ArchiveFilter(DateTimeFilterMixin, FilterSet): | ||||||
|  | 
 | ||||||
|  |     class Meta: | ||||||
|  |         model = Archive | ||||||
|  |         fields = { | ||||||
|  |             'id': ['exact', 'in', 'lte', 'gte'], | ||||||
|  |             'ticket': ['exact', 'in', 'lte', 'gte'], | ||||||
|  |             'time_create': ['exact', 'lte', 'gte'] | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TicketFilter(DateTimeFilterMixin, FilterSet): | ||||||
|  |     number = NumberFilter( | ||||||
|  |         field_name='number', | ||||||
|  |         widget=widgets.CSVWidget(), | ||||||
|  |     ) | ||||||
|  |     user = CharFilter( | ||||||
|  |         field_name='user__username' | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     class Meta: | ||||||
|  |         model = Ticket | ||||||
|  |         fields = { | ||||||
|  |             'id': ['exact', 'in', 'lte', 'gte'], | ||||||
|  |             'number': ['exact', 'contains', 'in', 'lte', 'gte'], | ||||||
|  |             'resolved': ['exact'], | ||||||
|  |             'user': ['exact'] | ||||||
|  |         } | ||||||
							
								
								
									
										13
									
								
								logs_collector/collector/api/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -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 | ||||||
							
								
								
									
										60
									
								
								logs_collector/collector/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,60 @@ | |||||||
|  | from rest_framework import serializers | ||||||
|  | 
 | ||||||
|  | from drf_spectacular.utils import extend_schema_field | ||||||
|  | from drf_spectacular.openapi import OpenApiTypes | ||||||
|  | 
 | ||||||
|  | from collector.models import Archive, Platform, Ticket | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @extend_schema_field(OpenApiTypes.NUMBER) | ||||||
|  | class TimestampField(serializers.Field): | ||||||
|  |     def to_representation(self, value) -> int: | ||||||
|  |         return value.timestamp() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @extend_schema_field(OpenApiTypes.NUMBER) | ||||||
|  | class JsTimestampField(serializers.Field): | ||||||
|  |     def to_representation(self, value) -> int: | ||||||
|  |         return round(value.timestamp()*1000) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PublicArchiveUploadSerializer(serializers.ModelSerializer): | ||||||
|  |     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'] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 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' | ||||||
|  |         ] | ||||||
							
								
								
									
										21
									
								
								logs_collector/collector/api/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,21 @@ | |||||||
|  | from django.urls import path, include | ||||||
|  | 
 | ||||||
|  | from rest_framework import routers | ||||||
|  | 
 | ||||||
|  | from . import views | ||||||
|  | 
 | ||||||
|  | # ▄▀█ █▀█ █ | ||||||
|  | # █▀█ █▀▀ █ | ||||||
|  | # -- -- -- | ||||||
|  | 
 | ||||||
|  | app_name = 'collector_api' | ||||||
|  | 
 | ||||||
|  | router = routers.DefaultRouter() | ||||||
|  | router.register(r'archives', views.ArchiveViewSet) | ||||||
|  | router.register(r'platforms', views.PlatformViewSet) | ||||||
|  | router.register(r'tickets', views.TicketViewSet) | ||||||
|  | 
 | ||||||
|  | urlpatterns = [ | ||||||
|  |     # CRUD: | ||||||
|  |     path('v1/', include(router.urls)), | ||||||
|  | ] | ||||||
							
								
								
									
										20
									
								
								logs_collector/collector/api/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,20 @@ | |||||||
|  | from django_filters import NumberFilter | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DateTimeFilterMixin: | ||||||
|  |     year__gte = NumberFilter( | ||||||
|  |         field_name='time_create', | ||||||
|  |         lookup_expr='year__gte' | ||||||
|  |     ) | ||||||
|  |     year__lte = NumberFilter( | ||||||
|  |         field_name='time_create', | ||||||
|  |         lookup_expr='year__lte' | ||||||
|  |     ) | ||||||
|  |     month__gte = NumberFilter( | ||||||
|  |         field_name='time_create', | ||||||
|  |         lookup_expr='month__gte' | ||||||
|  |     ) | ||||||
|  |     month__lte = NumberFilter( | ||||||
|  |         field_name='time_create', | ||||||
|  |         lookup_expr='month__lte' | ||||||
|  |     ) | ||||||
							
								
								
									
										94
									
								
								logs_collector/collector/api/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,94 @@ | |||||||
|  | from django.core.exceptions import ValidationError, ObjectDoesNotExist | ||||||
|  | 
 | ||||||
|  | from rest_framework import status | ||||||
|  | # 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 viewsets | ||||||
|  | from rest_framework import filters | ||||||
|  | 
 | ||||||
|  | from django_filters.rest_framework import DjangoFilterBackend | ||||||
|  | 
 | ||||||
|  | from collector.models import Archive, Ticket, Platform | ||||||
|  | 
 | ||||||
|  | from .filters import ArchiveFilter, TicketFilter | ||||||
|  | from .permissions import IsGuestUpload | ||||||
|  | from .serializers import ( | ||||||
|  |     PublicArchiveUploadSerializer, | ||||||
|  |     ArchiveSerializer, | ||||||
|  |     PlatformSerializer, | ||||||
|  |     TicketSerializer | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ArchiveViewSet(viewsets.ModelViewSet): | ||||||
|  |     queryset = Archive.objects.order_by('-time_create') | ||||||
|  |     serializer_class = ArchiveSerializer | ||||||
|  |     parser_classes = (MultiPartParser, FormParser) | ||||||
|  |     permission_classes = (IsGuestUpload, ) | ||||||
|  |     filter_backends = [DjangoFilterBackend] | ||||||
|  |     filterset_class = ArchiveFilter | ||||||
|  | 
 | ||||||
|  |     def create(self, request, *args, **kwargs): | ||||||
|  |         # ! upload-token protection: | ||||||
|  |         upload_token = request.headers.get('upload-token', '') | ||||||
|  |         if not request.user.is_authenticated and upload_token: | ||||||
|  |             try: | ||||||
|  |                 bound_ticket = Ticket.objects.get(token=upload_token) | ||||||
|  |                 if bound_ticket.resolved: | ||||||
|  |                     return Response( | ||||||
|  |                         {'error': f'ticket {upload_token} already resolved'}, | ||||||
|  |                         status=status.HTTP_423_LOCKED | ||||||
|  |                     ) | ||||||
|  |                 if bound_ticket.attempts <= 0: | ||||||
|  |                     return Response( | ||||||
|  |                         {'error': f'token {upload_token} expired'}, | ||||||
|  |                         status=status.HTTP_423_LOCKED | ||||||
|  |                     ) | ||||||
|  |                 bound_ticket.attempts -= 1 | ||||||
|  |                 bound_ticket.save() | ||||||
|  |                 # ? mixin bound ticket number to request.data from user | ||||||
|  |                 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 | ||||||
|  |                 ) | ||||||
|  |         elif not request.user.is_authenticated: | ||||||
|  |             return Response( | ||||||
|  |                 {'error': 'Header Upload-Token is required'}, | ||||||
|  |                 status=status.HTTP_401_UNAUTHORIZED | ||||||
|  |             ) | ||||||
|  |         # ! default create method: | ||||||
|  |         serializer = self.get_serializer(data=request.data) | ||||||
|  |         serializer.is_valid(raise_exception=True) | ||||||
|  |         self.perform_create(serializer) | ||||||
|  |         headers = self.get_success_headers(serializer.data) | ||||||
|  |         return Response( | ||||||
|  |             serializer.data, | ||||||
|  |             status=status.HTTP_201_CREATED, | ||||||
|  |             headers=headers | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 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, ) | ||||||
|  |     filter_backends = [DjangoFilterBackend, filters.SearchFilter] | ||||||
|  |     filterset_class = TicketFilter | ||||||
|  |     search_fields = ['number'] | ||||||
|  | 
 | ||||||
|  |     def perform_create(self, serializer): | ||||||
|  |         serializer.save(user=self.request.user) | ||||||
| @ -4,3 +4,4 @@ from django.apps import AppConfig | |||||||
| class CollectorConfig(AppConfig): | class CollectorConfig(AppConfig): | ||||||
|     default_auto_field = 'django.db.models.BigAutoField' |     default_auto_field = 'django.db.models.BigAutoField' | ||||||
|     name = 'collector' |     name = 'collector' | ||||||
|  |     verbose_name = 'Collector archives for analyse' | ||||||
|  | |||||||
| @ -1,11 +1,13 @@ | |||||||
| # Generated by Django 4.2 on 2023-07-28 14:40 | # Generated by Django 4.2 on 2023-08-15 03:58 | ||||||
| 
 | 
 | ||||||
| import collector.utils | import collector.utils | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| import django.core.files.storage | import django.core.files.storage | ||||||
|  | import django.core.validators | ||||||
| from django.db import migrations, models | from django.db import migrations, models | ||||||
| import django.db.models.deletion | import django.db.models.deletion | ||||||
| import pathlib | import pathlib | ||||||
|  | import uuid | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Migration(migrations.Migration): | class Migration(migrations.Migration): | ||||||
| @ -21,7 +23,7 @@ class Migration(migrations.Migration): | |||||||
|             name='Platform', |             name='Platform', | ||||||
|             fields=[ |             fields=[ | ||||||
|                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|                 ('name', models.CharField(max_length=20)), |                 ('name', models.CharField(max_length=20, unique=True)), | ||||||
|                 ('pretty_name', models.CharField(max_length=20)), |                 ('pretty_name', models.CharField(max_length=20)), | ||||||
|             ], |             ], | ||||||
|         ), |         ), | ||||||
| @ -29,26 +31,29 @@ class Migration(migrations.Migration): | |||||||
|             name='Ticket', |             name='Ticket', | ||||||
|             fields=[ |             fields=[ | ||||||
|                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|                 ('number', models.IntegerField()), |                 ('number', models.IntegerField(db_index=True, unique=True)), | ||||||
|                 ('resolved', models.BooleanField(default=False)), |                 ('resolved', models.BooleanField(default=False)), | ||||||
|                 ('note', models.TextField(blank=True)), |                 ('note', models.TextField(blank=True)), | ||||||
|  |                 ('token', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), | ||||||
|  |                 ('attempts', models.IntegerField(default=5, validators=[django.core.validators.MaxValueValidator(10), django.core.validators.MinValueValidator(0)])), | ||||||
|                 ('time_create', models.DateTimeField(auto_now_add=True)), |                 ('time_create', models.DateTimeField(auto_now_add=True)), | ||||||
|                 ('time_update', models.DateTimeField(auto_now=True)), |                 ('time_update', models.DateTimeField(auto_now=True)), | ||||||
|                 ('platform', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='collector.platform')), |                 ('platform', models.ForeignKey(db_column='platform_name', on_delete=django.db.models.deletion.CASCADE, to='collector.platform', to_field='name')), | ||||||
|                 ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), |                 ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), | ||||||
|             ], |             ], | ||||||
|  |             options={ | ||||||
|  |                 'ordering': ['-time_create'], | ||||||
|  |             }, | ||||||
|         ), |         ), | ||||||
|         migrations.CreateModel( |         migrations.CreateModel( | ||||||
|             name='Archive', |             name='Archive', | ||||||
|             fields=[ |             fields=[ | ||||||
|                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |                 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|                 ('file', models.FileField(blank=True, null=True, storage=django.core.files.storage.FileSystemStorage(base_url='/archives/', location=pathlib.PurePosixPath('/home/stepan/Documents/Dev/ISPsystem/logs-collector/logs_collector/archives')), upload_to=collector.utils.logs_dir_path)), |                 ('file', models.FileField(blank=True, null=True, storage=django.core.files.storage.FileSystemStorage(base_url='/archives/', location=pathlib.PurePosixPath('/home/stepan/Documents/Dev/ISPsystem/logs-collector/logs_collector/archives')), upload_to=collector.utils.logs_dir_path)), | ||||||
|                 ('size', models.CharField(blank=True, max_length=50)), |                 ('md5', models.CharField(editable=False, max_length=1024)), | ||||||
|                 ('sha1', models.CharField(editable=False, max_length=1024)), |  | ||||||
|                 ('time_create', models.DateTimeField(auto_now_add=True)), |                 ('time_create', models.DateTimeField(auto_now_add=True)), | ||||||
|                 ('time_update', models.DateTimeField(auto_now=True)), |                 ('time_update', models.DateTimeField(auto_now=True)), | ||||||
|                 ('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='collector.ticket')), |                 ('ticket', models.ForeignKey(db_column='ticket_number', on_delete=django.db.models.deletion.CASCADE, to='collector.ticket', to_field='number')), | ||||||
|                 ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), |  | ||||||
|             ], |             ], | ||||||
|         ), |         ), | ||||||
|     ] |     ] | ||||||
|  | |||||||
| @ -1,27 +0,0 @@ | |||||||
| # Generated by Django 4.2 on 2023-08-05 11:14 |  | ||||||
| 
 |  | ||||||
| from django.db import migrations, models |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
| 
 |  | ||||||
|     dependencies = [ |  | ||||||
|         ('collector', '0001_initial'), |  | ||||||
|     ] |  | ||||||
| 
 |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterModelOptions( |  | ||||||
|             name='ticket', |  | ||||||
|             options={'ordering': ['-time_create']}, |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name='archive', |  | ||||||
|             name='size', |  | ||||||
|             field=models.CharField(blank=True, editable=False, max_length=50), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name='ticket', |  | ||||||
|             name='number', |  | ||||||
|             field=models.IntegerField(db_index=True, unique=True), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,19 +0,0 @@ | |||||||
| # Generated by Django 4.2 on 2023-08-08 05:52 |  | ||||||
| 
 |  | ||||||
| from django.db import migrations, models |  | ||||||
| import django.db.models.deletion |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
| 
 |  | ||||||
|     dependencies = [ |  | ||||||
|         ('collector', '0002_alter_ticket_options_alter_archive_size_and_more'), |  | ||||||
|     ] |  | ||||||
| 
 |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name='archive', |  | ||||||
|             name='ticket', |  | ||||||
|             field=models.ForeignKey(db_column='ticket_number', on_delete=django.db.models.deletion.CASCADE, to='collector.ticket', to_field='number'), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,22 +0,0 @@ | |||||||
| # Generated by Django 4.2 on 2023-08-08 09:17 |  | ||||||
| 
 |  | ||||||
| from django.db import migrations |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
| 
 |  | ||||||
|     dependencies = [ |  | ||||||
|         ('collector', '0003_alter_archive_ticket'), |  | ||||||
|     ] |  | ||||||
| 
 |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RenameField( |  | ||||||
|             model_name='archive', |  | ||||||
|             old_name='sha1', |  | ||||||
|             new_name='md5', |  | ||||||
|         ), |  | ||||||
|         migrations.RemoveField( |  | ||||||
|             model_name='archive', |  | ||||||
|             name='size', |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,33 +0,0 @@ | |||||||
| # Generated by Django 4.2 on 2023-08-08 11:16 |  | ||||||
| 
 |  | ||||||
| from django.conf import settings |  | ||||||
| import django.core.validators |  | ||||||
| from django.db import migrations, models |  | ||||||
| import django.db.models.deletion |  | ||||||
| import uuid |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
| 
 |  | ||||||
|     dependencies = [ |  | ||||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), |  | ||||||
|         ('collector', '0004_rename_sha1_archive_md5_remove_archive_size'), |  | ||||||
|     ] |  | ||||||
| 
 |  | ||||||
|     operations = [ |  | ||||||
|         migrations.CreateModel( |  | ||||||
|             name='Token', |  | ||||||
|             fields=[ |  | ||||||
|                 ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), |  | ||||||
|                 ('expires', models.IntegerField(default=5, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(1)])), |  | ||||||
|                 ('blocked', models.BooleanField(default=False)), |  | ||||||
|                 ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), |  | ||||||
|             ], |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name='archive', |  | ||||||
|             name='token', |  | ||||||
|             field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='collector.token'), |  | ||||||
|             preserve_default=False, |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,47 +0,0 @@ | |||||||
| # Generated by Django 4.2 on 2023-08-08 16:52 |  | ||||||
| 
 |  | ||||||
| import django.core.validators |  | ||||||
| from django.db import migrations, models |  | ||||||
| import django.db.models.deletion |  | ||||||
| import uuid |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
| 
 |  | ||||||
|     dependencies = [ |  | ||||||
|         ('collector', '0005_token_archive_token'), |  | ||||||
|     ] |  | ||||||
| 
 |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RemoveField( |  | ||||||
|             model_name='archive', |  | ||||||
|             name='token', |  | ||||||
|         ), |  | ||||||
|         migrations.RemoveField( |  | ||||||
|             model_name='archive', |  | ||||||
|             name='user', |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name='ticket', |  | ||||||
|             name='token', |  | ||||||
|             field=models.UUIDField(default=uuid.uuid4, editable=False), |  | ||||||
|         ), |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name='ticket', |  | ||||||
|             name='upload', |  | ||||||
|             field=models.IntegerField(default=5, validators=[django.core.validators.MaxValueValidator(10), django.core.validators.MinValueValidator(1)]), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name='platform', |  | ||||||
|             name='name', |  | ||||||
|             field=models.CharField(max_length=20, unique=True), |  | ||||||
|         ), |  | ||||||
|         migrations.AlterField( |  | ||||||
|             model_name='ticket', |  | ||||||
|             name='platform', |  | ||||||
|             field=models.ForeignKey(db_column='platform_name', on_delete=django.db.models.deletion.CASCADE, to='collector.platform', to_field='name'), |  | ||||||
|         ), |  | ||||||
|         migrations.DeleteModel( |  | ||||||
|             name='Token', |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -1,18 +0,0 @@ | |||||||
| # Generated by Django 4.2 on 2023-08-08 17:08 |  | ||||||
| 
 |  | ||||||
| from django.db import migrations |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
| 
 |  | ||||||
|     dependencies = [ |  | ||||||
|         ('collector', '0006_remove_archive_token_remove_archive_user_and_more'), |  | ||||||
|     ] |  | ||||||
| 
 |  | ||||||
|     operations = [ |  | ||||||
|         migrations.RenameField( |  | ||||||
|             model_name='ticket', |  | ||||||
|             old_name='upload', |  | ||||||
|             new_name='attempts', |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
| @ -41,7 +41,7 @@ class Archive(models.Model): | |||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     def save(self, *args, **kwargs): |     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: |         with self.file.open('rb') as f: | ||||||
|             md5 = hashlib.md5() |             md5 = hashlib.md5() | ||||||
|             for byte_block in iter(lambda: f.read(4096), b""): |             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) |     number = models.IntegerField(unique=True, db_index=True) | ||||||
|     resolved = models.BooleanField(default=False) |     resolved = models.BooleanField(default=False) | ||||||
|     note = models.TextField(blank=True) |     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=[ |     attempts = models.IntegerField(default=5, validators=[ | ||||||
|             MaxValueValidator(10), |             MaxValueValidator(10), | ||||||
|             MinValueValidator(0) |             MinValueValidator(0) | ||||||
|  | |||||||
| @ -1,11 +0,0 @@ | |||||||
| from rest_framework import serializers |  | ||||||
| 
 |  | ||||||
| from .models import Archive |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class PublicArchiveUploadSerializer(serializers.ModelSerializer): |  | ||||||
|     ticket = serializers.ReadOnlyField(source='ticket.token') |  | ||||||
| 
 |  | ||||||
|     class Meta: |  | ||||||
|         model = Archive |  | ||||||
|         fields = ['file', 'ticket'] |  | ||||||
| @ -12,7 +12,7 @@ $(function () { | |||||||
|         const archiveListElement = $(this).attr("data-jq-archive-target"); |         const archiveListElement = $(this).attr("data-jq-archive-target"); | ||||||
|         const delUrl = $(this).attr("href"); |         const delUrl = $(this).attr("href"); | ||||||
|         $.ajax({ |         $.ajax({ | ||||||
|             type: "delete", |             type: "DELETE", | ||||||
|             url: delUrl, |             url: delUrl, | ||||||
|             headers: { |             headers: { | ||||||
|                 "X-CSRFToken":CSRF, |                 "X-CSRFToken":CSRF, | ||||||
| @ -21,12 +21,12 @@ $(function () { | |||||||
|             // beforeSend: function(xhr) {
 |             // beforeSend: function(xhr) {
 | ||||||
|             //     xhr.setRequestHeader("X-CSRFToken", csrf);
 |             //     xhr.setRequestHeader("X-CSRFToken", csrf);
 | ||||||
|             // },
 |             // },
 | ||||||
|             success: function (response) { |             success: function (data, textStatus, jqXHR) { | ||||||
|                 console.log(response.status); |                 console.log(jqXHR.status); | ||||||
|                 $(archiveListElement).hide(1500); |                 $(archiveListElement).hide(1500); | ||||||
|             }, |             }, | ||||||
|             error: function (response) { |             error: function (data, textStatus, jqXHR) { | ||||||
|                 console.log(response.status); |                 console.log(jqXHR.status); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     }); |     }); | ||||||
| @ -37,16 +37,13 @@ $(function () { | |||||||
|         let resolved = false; |         let resolved = false; | ||||||
|         let ticketStateUrl = $(this).attr("ticket-state-url") |         let ticketStateUrl = $(this).attr("ticket-state-url") | ||||||
|         if ($(this).attr("ticket-state-switch") === "1") { |         if ($(this).attr("ticket-state-switch") === "1") { | ||||||
|             console.log('Find it!!!') |  | ||||||
|             resolved = true; |  | ||||||
|             $(this).attr("ticket-state-switch", "0");  // disable
 |             $(this).attr("ticket-state-switch", "0");  // disable
 | ||||||
|         } else { |         } else { | ||||||
|             resolved = false; |             resolved = true; | ||||||
|             $(this).attr("ticket-state-switch", "1");  // enable
 |             $(this).attr("ticket-state-switch", "1");  // enable
 | ||||||
|         } |         } | ||||||
|         console.log(resolved) |  | ||||||
|         $.ajax({ |         $.ajax({ | ||||||
|             type: "POST", |             type: "PATCH", | ||||||
|             url: ticketStateUrl, |             url: ticketStateUrl, | ||||||
|             headers: { |             headers: { | ||||||
|                 "X-CSRFToken":CSRF, |                 "X-CSRFToken":CSRF, | ||||||
| @ -57,11 +54,12 @@ $(function () { | |||||||
|             data: JSON.stringify({ |             data: JSON.stringify({ | ||||||
|                 resolved: resolved, |                 resolved: resolved, | ||||||
|             }), |             }), | ||||||
|             success: function (response) { |             success: function (data, textStatus, jqXHR) { | ||||||
|                 console.log(response.resolved) |                 console.log(jqXHR.status) | ||||||
|             }, |             }, | ||||||
|             error: function (response) { |             error: function (data, textStatus, jqXHR) { | ||||||
|                 console.log(response.resolved) |                 console.log(data) | ||||||
|  |                 console.log(jqXHR.status) | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     }); |     }); | ||||||
| @ -80,16 +78,16 @@ $(function () { | |||||||
|                 'X-CSRFToken':CSRF, |                 'X-CSRFToken':CSRF, | ||||||
|                 'Content-Type':'application/json' |                 'Content-Type':'application/json' | ||||||
|             }, |             }, | ||||||
|             success: function (response) { |             success: function (data, textStatus, jqXHR) { | ||||||
|                 console.log(response.status); |                 console.log(jqXHR.status); | ||||||
|                 if (delDiv.length) { |                 if (delDiv.length) { | ||||||
|                     delDiv.hide(1500); |                     delDiv.hide(1500); | ||||||
|                 } else { |                 } else { | ||||||
|                     window.location.href = redirectUrl; |                     window.location.href = redirectUrl; | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|             error: function (response) { |             error: function (data, textStatus, jqXHR) { | ||||||
|                 console.log(response.status); |                 console.log(jqXHR.status); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     }); |     }); | ||||||
|  | |||||||
| @ -1,45 +1,14 @@ | |||||||
|  | {% extends 'base.html' %} | ||||||
| {% load static %} | {% load static %} | ||||||
| <!DOCTYPE html> | 
 | ||||||
| <html lang="en" data-bs-theme="auto"> | {% block collector_head %} | ||||||
|   <head> |   <title>{% block title %}{% endblock title %}</title> | ||||||
|     <meta charset="UTF-8" /> | {% endblock collector_head %} | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | 
 | ||||||
|     <link | {% block collector_content %} | ||||||
|       rel="stylesheet" |  | ||||||
|       href="{% static 'collector/css/bootstrap.min.css' %}" |  | ||||||
|       rel="stylesheet" |  | ||||||
|     > |  | ||||||
|     <link |  | ||||||
|       rel="apple-touch-icon" |  | ||||||
|       sizes="180x180" |  | ||||||
|       href="{% static 'collector/img/apple-touch-icon.png' %}" |  | ||||||
|     > |  | ||||||
|     <link |  | ||||||
|       rel="icon" |  | ||||||
|       type="image/png" |  | ||||||
|       sizes="32x32" |  | ||||||
|       href="{% static 'collector/img/favicon-32x32.png' %}" |  | ||||||
|     > |  | ||||||
|     <link |  | ||||||
|       rel="icon" |  | ||||||
|       type="image/png" |  | ||||||
|       sizes="16x16" |  | ||||||
|       href="{% static 'collector/img/favicon-16x16.png' %}" |  | ||||||
|     > |  | ||||||
|     <link  |  | ||||||
|       rel="manifest" |  | ||||||
|       href="{% static 'collector/img/site.webmanifest' %}" |  | ||||||
|     > |  | ||||||
|     <link  |  | ||||||
|       rel="stylesheet" |  | ||||||
|       href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" |  | ||||||
|     > |  | ||||||
|     {% block title %}{% endblock title %} |  | ||||||
|   </head> |  | ||||||
|   <body> |  | ||||||
|   <header> |   <header> | ||||||
|     <section> |     <section> | ||||||
|         {% include 'collector/includes/navigation.html' %} |       {% include 'includes/navigation.html' %} | ||||||
|     </section> |     </section> | ||||||
|   </header> |   </header> | ||||||
|   <main> |   <main> | ||||||
| @ -52,10 +21,10 @@ | |||||||
|       {% block footer %}{% endblock footer %} |       {% block footer %}{% endblock footer %} | ||||||
|     </section> |     </section> | ||||||
|   </footer> |   </footer> | ||||||
|     <script src="{% static 'collector/js/bootstrap.bundle.min.js' %}"></script> | {% endblock collector_content %} | ||||||
|     <script src="{% static 'collector/js/bs.theme.mode.js' %}"></script> | 
 | ||||||
|  | {% block collector_scripts %} | ||||||
|   <script src="{% static 'collector/js/jquery-3.7.0.min.js' %}"></script> |   <script src="{% static 'collector/js/jquery-3.7.0.min.js' %}"></script> | ||||||
|   {% block bs %}{% endblock bs %} |   {% block bs %}{% endblock bs %} | ||||||
|   {% block jquery %}{% endblock jquery %} |   {% block jquery %}{% endblock jquery %} | ||||||
|   </body> | {% endblock collector_scripts %} | ||||||
| </html> |  | ||||||
|  | |||||||
| @ -30,7 +30,7 @@ | |||||||
|         >Cancel |         >Cancel | ||||||
|         </button> |         </button> | ||||||
|         <a |         <a | ||||||
|           href="{% url 'collector:download' archive.file %}" |           href="{% url 'collector_api:archive-detail' archive.id %}" | ||||||
|           type="button" |           type="button" | ||||||
|           class="btn btn-danger btn-archive-eraser" |           class="btn btn-danger btn-archive-eraser" | ||||||
|           data-bs-dismiss="modal" |           data-bs-dismiss="modal" | ||||||
|  | |||||||
| @ -27,7 +27,7 @@ | |||||||
|         </button> |         </button> | ||||||
|         <a |         <a | ||||||
|           type="button" |           type="button" | ||||||
|           href="{% url 'collector:ajax_delete_ticket' ticket.number %}" |           href="{% url 'collector_api:ticket-detail' ticket.number %}" | ||||||
|           class="btn btn-danger btn-ticket-del" |           class="btn btn-danger btn-ticket-del" | ||||||
|           data-bs-dismiss="modal" |           data-bs-dismiss="modal" | ||||||
|           data-jq-ticket-del-target="#div-ticket-{{ ticket.number }}" |           data-jq-ticket-del-target="#div-ticket-{{ ticket.number }}" | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ | |||||||
|     type="checkbox" |     type="checkbox" | ||||||
|     role="switch" |     role="switch" | ||||||
|     name="ticket-state" |     name="ticket-state" | ||||||
|     ticket-state-url="{% url 'collector:ajax_update_state_ticket' ticket.platform.name ticket.number %}" |     ticket-state-url="{% url 'collector_api:ticket-detail' ticket.number %}" | ||||||
|     {% if ticket.resolved %} ticket-state-switch="1" {% endif %} |     {% if ticket.resolved %} ticket-state-switch="1" {% endif %} | ||||||
|     {% if ticket.resolved %} checked {% endif %}> |     {% if ticket.resolved %} checked {% endif %}> | ||||||
| </div> | </div> | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| {% extends 'collector/base.html' %} | {% extends 'collector/base.html' %} | ||||||
| {% load static %} | {% load static %} | ||||||
| {% load collector_extras %} | {% load collector_extras %} | ||||||
| {% block title %}<title>{{ title }}</title>{% endblock title %} | {% block title %} {{ title }} {% endblock title %} | ||||||
| {% block main %}  | {% block main %}  | ||||||
| <div class="container mt-3"> | <div class="container mt-3"> | ||||||
|   <div class="row"> |   <div class="row"> | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| {% extends 'collector/base.html' %} | {% extends 'collector/base.html' %} | ||||||
| {% load static %} | {% load static %} | ||||||
| {% load crispy_forms_tags %} | {% load crispy_forms_tags %} | ||||||
| {% block title %}<title>{{ title }}</title>{% endblock title %} | {% block title %} {{ title }} {% endblock title %} | ||||||
| {% block main %} | {% block main %} | ||||||
| <div class="container mt-3"> | <div class="container mt-3"> | ||||||
|     <div class="card"> |     <div class="card"> | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| {% extends 'collector/base.html' %} | {% extends 'collector/base.html' %} | ||||||
| {% load static %} | {% load static %} | ||||||
| {% load collector_extras %} | {% load collector_extras %} | ||||||
| {% block title %}<title>{{ title }}</title>{% endblock title %} | {% block title %} {{ title }} {% endblock title %} | ||||||
| {% block main %} | {% block main %} | ||||||
| <div class="container mt-3"> | <div class="container mt-3"> | ||||||
|   {% csrf_token %} |   {% csrf_token %} | ||||||
|  | |||||||
| @ -1,20 +1,14 @@ | |||||||
| from django.urls import path, include | from django.urls import path | ||||||
| 
 |  | ||||||
| from rest_framework import routers |  | ||||||
| 
 | 
 | ||||||
| from . import views | from . import views | ||||||
| 
 | 
 | ||||||
| app_name = 'collector' | app_name = 'collector' | ||||||
| 
 | 
 | ||||||
| router = routers.DefaultRouter() |  | ||||||
| router.register(r'archives', views.PublicArchiveUploadViewSet) |  | ||||||
| 
 |  | ||||||
| urlpatterns = [ |  | ||||||
| 
 |  | ||||||
| # █░█░█ █▀▀ █▄▄ | # █░█░█ █▀▀ █▄▄ | ||||||
| # ▀▄▀▄▀ ██▄ █▄█ | # ▀▄▀▄▀ ██▄ █▄█ | ||||||
| # -- -- -- -- -- | # -- -- -- -- -- | ||||||
| 
 | 
 | ||||||
|  | urlpatterns = [ | ||||||
|     # CREATE: |     # CREATE: | ||||||
|     path( |     path( | ||||||
|         'tickets/create/', |         'tickets/create/', | ||||||
| @ -53,28 +47,4 @@ urlpatterns = [ | |||||||
|         views.UpdateTicket.as_view(), |         views.UpdateTicket.as_view(), | ||||||
|         name='update' |         name='update' | ||||||
|     ), |     ), | ||||||
| 
 |  | ||||||
|     # ▄▀█ ░░█ ▄▀█ ▀▄▀ |  | ||||||
|     # █▀█ █▄█ █▀█ █░█ |  | ||||||
|     # -- -- -- -- -- |  | ||||||
| 
 |  | ||||||
|     # UPDATE: |  | ||||||
|     path( |  | ||||||
|         'ajax/tickets/update/<slug:platform>/<int:ticket>/', |  | ||||||
|         views.UpdateTicketStateHandler.as_view(), |  | ||||||
|         name='ajax_update_state_ticket' |  | ||||||
|     ), |  | ||||||
|     # DELETE: |  | ||||||
|     path( |  | ||||||
|         'ajax/tickets/delete/<int:ticket>/', |  | ||||||
|         views.DeleteTicketHandler.as_view(), |  | ||||||
|         name='ajax_delete_ticket' |  | ||||||
|     ), |  | ||||||
| 
 |  | ||||||
|     # ▄▀█ █▀█ █ |  | ||||||
|     # █▀█ █▀▀ █ |  | ||||||
|     # -- -- -- |  | ||||||
| 
 |  | ||||||
|     # CREATE: |  | ||||||
|     path('api/v1/', include(router.urls)) |  | ||||||
| ] | ] | ||||||
|  | |||||||
| @ -2,11 +2,14 @@ import os | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def logs_dir_path(instance, filename): | def logs_dir_path(instance, filename): | ||||||
|     # file will be uploaded to |     """ | ||||||
|     # MEDIA_ROOT_FOR_SENSITIVE_FILES/<ticket>/<filename> |         file will be uploaded to | ||||||
|     return f'{instance.ticket}/{filename}' |         MEDIA_ROOT_FOR_SENSITIVE_FILES/<ticket-token>/<filename> | ||||||
|  |     """ | ||||||
|  |     return f'{instance.ticket.number}/{filename}' | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | # deprecated | ||||||
| def get_file_size(file_path, unit='bytes'): | def get_file_size(file_path, unit='bytes'): | ||||||
|     file_size = os.path.getsize(file_path) |     file_size = os.path.getsize(file_path) | ||||||
|     exponents_map = {'bytes': 0, 'kb': 1, 'mb': 2, 'gb': 3} |     exponents_map = {'bytes': 0, 'kb': 1, 'mb': 2, 'gb': 3} | ||||||
| @ -18,6 +21,7 @@ def get_file_size(file_path, unit='bytes'): | |||||||
|         return round(size, 3) |         return round(size, 3) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | # deprecated | ||||||
| def is_ajax(request): | def is_ajax(request): | ||||||
|     if request.headers.get('X-Requested-With') == 'XMLHttpRequest': |     if request.headers.get('X-Requested-With') == 'XMLHttpRequest': | ||||||
|         return True |         return True | ||||||
|  | |||||||
| @ -1,27 +1,21 @@ | |||||||
| import json |  | ||||||
| from django.core.exceptions import ValidationError |  | ||||||
| from django.contrib.auth.mixins import LoginRequiredMixin | from django.contrib.auth.mixins import LoginRequiredMixin | ||||||
| from django.http import FileResponse, JsonResponse | from django.http import FileResponse | ||||||
| from django.views import generic | from django.views import generic | ||||||
| from django.views.generic.detail import SingleObjectMixin | from django.views.generic.detail import SingleObjectMixin | ||||||
| from django.urls import reverse_lazy |  | ||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
| 
 | 
 | ||||||
| from rest_framework import status | from two_factor.views import OTPRequiredMixin | ||||||
| from rest_framework.response import Response |  | ||||||
| from rest_framework.parsers import FormParser, MultiPartParser |  | ||||||
| 
 | 
 | ||||||
| from rest_framework import mixins |  | ||||||
| from rest_framework.viewsets import GenericViewSet |  | ||||||
| 
 |  | ||||||
| from .models import Archive, Ticket |  | ||||||
| from .forms import TicketForm | from .forms import TicketForm | ||||||
| from .utils import PageTitleViewMixin, is_ajax | from .models import Archive, Ticket | ||||||
| 
 | from .utils import PageTitleViewMixin | ||||||
| from .serializers import PublicArchiveUploadSerializer |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ArchiveHandlerView(LoginRequiredMixin, SingleObjectMixin, generic.View): | class ArchiveHandlerView( | ||||||
|  |         OTPRequiredMixin, | ||||||
|  |         LoginRequiredMixin, | ||||||
|  |         SingleObjectMixin, | ||||||
|  |         generic.View): | ||||||
|     model = Archive |     model = Archive | ||||||
|     slug_field = 'file' |     slug_field = 'file' | ||||||
|     slug_url_kwarg = 'path' |     slug_url_kwarg = 'path' | ||||||
| @ -30,12 +24,6 @@ class ArchiveHandlerView(LoginRequiredMixin, SingleObjectMixin, generic.View): | |||||||
|         self.object = self.get_object() |         self.object = self.get_object() | ||||||
|         return FileResponse(self.object.file) |         return FileResponse(self.object.file) | ||||||
| 
 | 
 | ||||||
|     def delete(self, request, path): |  | ||||||
|         if is_ajax(request): |  | ||||||
|             self.object = self.get_object() |  | ||||||
|             self.object.delete() |  | ||||||
|             return JsonResponse({'file': path}, status=status.HTTP_200_OK) |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView): | class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView): | ||||||
|     model = Ticket |     model = Ticket | ||||||
| @ -61,12 +49,11 @@ class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView): | |||||||
|         return f'{self.title} - {self.kwargs.get("ticket", "update")}' |         return f'{self.title} - {self.kwargs.get("ticket", "update")}' | ||||||
| 
 | 
 | ||||||
|     def form_valid(self, form): |     def form_valid(self, form): | ||||||
|         print(self.request.user) |  | ||||||
|         form.instance.user = self.request.user |         form.instance.user = self.request.user | ||||||
|         return super().form_valid(form) |         return super().form_valid(form) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ListAllTickets(PageTitleViewMixin, generic.ListView): | class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView): | ||||||
|     model = Ticket |     model = Ticket | ||||||
|     template_name = 'collector/tickets.html' |     template_name = 'collector/tickets.html' | ||||||
|     context_object_name = 'tickets' |     context_object_name = 'tickets' | ||||||
| @ -85,13 +72,17 @@ class ListAllTickets(PageTitleViewMixin, generic.ListView): | |||||||
|             queryset = self.model.objects.filter( |             queryset = self.model.objects.filter( | ||||||
|                 Q(number__in=query_list) | Q(number__icontains=query_list[0]) |                 Q(number__in=query_list) | Q(number__icontains=query_list[0]) | ||||||
|             ) |             ) | ||||||
|             self.paginate_by = 100  # fake disable pagination) |             self.paginate_by = 100  # ? fake disable pagination) | ||||||
|             return queryset |             return queryset | ||||||
| 
 | 
 | ||||||
|         return super().get_queryset() |         return super().get_queryset() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ListPlatformTickets(PageTitleViewMixin, generic.ListView): | class ListPlatformTickets( | ||||||
|  |         LoginRequiredMixin, | ||||||
|  |         PageTitleViewMixin, | ||||||
|  |         generic.ListView | ||||||
|  |         ): | ||||||
|     model = Ticket |     model = Ticket | ||||||
|     template_name = 'collector/tickets.html' |     template_name = 'collector/tickets.html' | ||||||
|     context_object_name = 'tickets' |     context_object_name = 'tickets' | ||||||
| @ -107,7 +98,7 @@ class ListPlatformTickets(PageTitleViewMixin, generic.ListView): | |||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class DetailTicket(PageTitleViewMixin, generic.DetailView): | class DetailTicket(LoginRequiredMixin, PageTitleViewMixin, generic.DetailView): | ||||||
|     model = Ticket |     model = Ticket | ||||||
|     template_name = 'collector/ticket.html' |     template_name = 'collector/ticket.html' | ||||||
|     context_object_name = 'ticket' |     context_object_name = 'ticket' | ||||||
| @ -116,109 +107,3 @@ class DetailTicket(PageTitleViewMixin, generic.DetailView): | |||||||
| 
 | 
 | ||||||
|     def get_title(self, **kwargs): |     def get_title(self, **kwargs): | ||||||
|         return f'{self.title} - {self.kwargs.get("ticket", "show")}' |         return f'{self.title} - {self.kwargs.get("ticket", "show")}' | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class DeleteTicket(PageTitleViewMixin, generic.DeleteView): |  | ||||||
|     model = Ticket |  | ||||||
|     template_name = 'collector/ticket_delete.html' |  | ||||||
|     context_object_name = 'ticket' |  | ||||||
|     slug_field = 'number' |  | ||||||
|     slug_url_kwarg = 'ticket' |  | ||||||
|     success_url = reverse_lazy('tickets') |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class UpdateTicketStateHandler(SingleObjectMixin, generic.View): |  | ||||||
|     model = Ticket |  | ||||||
|     slug_field = 'number' |  | ||||||
|     slug_url_kwarg = 'ticket' |  | ||||||
| 
 |  | ||||||
|     def post(self, request, **kwargs): |  | ||||||
|         if is_ajax(request): |  | ||||||
|             self.object = self.get_object() |  | ||||||
|             if request.body: |  | ||||||
|                 data = json.loads(request.body) |  | ||||||
|                 resolved_field = data.get('resolved') |  | ||||||
|                 if isinstance(resolved_field, bool): |  | ||||||
|                     self.object.resolved = not resolved_field |  | ||||||
|                     self.object.save() |  | ||||||
|                     return JsonResponse( |  | ||||||
|                         {'resolved': not resolved_field}, |  | ||||||
|                         status=status.HTTP_201_CREATED |  | ||||||
|                     ) |  | ||||||
|                 return JsonResponse( |  | ||||||
|                     {'resolved': 'must be a boolean'}, |  | ||||||
|                     status=status.HTTP_400_BAD_REQUEST |  | ||||||
|                 ) |  | ||||||
|         return JsonResponse( |  | ||||||
|             {'error': 'header XMLHttpRequest is required'}, |  | ||||||
|             status=status.HTTP_406_NOT_ACCEPTABLE |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class DeleteTicketHandler(SingleObjectMixin, generic.View): |  | ||||||
|     model = Ticket |  | ||||||
|     slug_field = 'number' |  | ||||||
|     slug_url_kwarg = 'ticket' |  | ||||||
| 
 |  | ||||||
|     def delete(self, request, ticket): |  | ||||||
|         if is_ajax(request): |  | ||||||
|             self.object = self.get_object() |  | ||||||
|             self.object.delete() |  | ||||||
|             return JsonResponse( |  | ||||||
|                 {'status': status.HTTP_200_OK}, |  | ||||||
|                 status=status.HTTP_200_OK |  | ||||||
|             ) |  | ||||||
|         return JsonResponse( |  | ||||||
|             {'error': 'header XMLHttpRequest is required'}, |  | ||||||
|             status=status.HTTP_406_NOT_ACCEPTABLE |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class PublicArchiveUploadViewSet(mixins.CreateModelMixin, GenericViewSet): |  | ||||||
|     queryset = Archive.objects.order_by('-time_create') |  | ||||||
|     serializer_class = PublicArchiveUploadSerializer |  | ||||||
|     parser_classes = (MultiPartParser, FormParser) |  | ||||||
| 
 |  | ||||||
|     def create(self, request, *args, **kwargs): |  | ||||||
|         # ! upload-token protection: |  | ||||||
|         upload_token = request.headers.get('upload-token', '') |  | ||||||
|         if upload_token: |  | ||||||
|             try: |  | ||||||
|                 bound_ticket = Ticket.objects.get(token=upload_token) |  | ||||||
|                 if bound_ticket.resolved: |  | ||||||
|                     return Response( |  | ||||||
|                         {'error': f'ticket {upload_token} already resolved'}, |  | ||||||
|                         status=status.HTTP_423_LOCKED |  | ||||||
|                     ) |  | ||||||
|                 if bound_ticket.attempts <= 0: |  | ||||||
|                     return Response( |  | ||||||
|                         {'error': f'token {upload_token} expired'}, |  | ||||||
|                         status=status.HTTP_423_LOCKED |  | ||||||
|                     ) |  | ||||||
|                 bound_ticket.attempts -= 1 |  | ||||||
|                 bound_ticket.save() |  | ||||||
|                 # ? mixin bound ticket to request.data from user |  | ||||||
|                 request.data['ticket'] = bound_ticket |  | ||||||
|             except ValidationError: |  | ||||||
|                 return Response( |  | ||||||
|                     {'error': f'token {upload_token} is not valid'}, |  | ||||||
|                     status=status.HTTP_403_FORBIDDEN |  | ||||||
|                 ) |  | ||||||
|         else: |  | ||||||
|             return Response( |  | ||||||
|                 {'error': 'Header Upload-Token is required'}, |  | ||||||
|                 status=status.HTTP_401_UNAUTHORIZED |  | ||||||
|             ) |  | ||||||
|         # ! default create method: |  | ||||||
|         serializer = self.get_serializer(data=request.data) |  | ||||||
|         serializer.is_valid(raise_exception=True) |  | ||||||
|         self.perform_create(serializer) |  | ||||||
|         headers = self.get_success_headers(serializer.data) |  | ||||||
|         return Response( |  | ||||||
|             serializer.data, |  | ||||||
|             status=status.HTTP_201_CREATED, |  | ||||||
|             headers=headers |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|     def perform_create(self, serializer): |  | ||||||
|         serializer.save(ticket=self.request.data['ticket']) |  | ||||||
|  | |||||||
| @ -1,10 +1,12 @@ | |||||||
| import environ | import environ | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  | from datetime import timedelta | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| env = environ.Env( | env = environ.Env( | ||||||
|     # set casting default value |     # set casting default value | ||||||
|     DEBUG=(bool, False) |     DEBUG=(bool, False), | ||||||
|  |     SECRET_KEY=(str, 'j9QGbvM9Z4otb47') | ||||||
| ) | ) | ||||||
| # Build paths inside the project like this: BASE_DIR / 'subdir'. | # Build paths inside the project like this: BASE_DIR / 'subdir'. | ||||||
| BASE_DIR = Path(__file__).resolve().parent.parent | BASE_DIR = Path(__file__).resolve().parent.parent | ||||||
| @ -34,9 +36,18 @@ INSTALLED_APPS = [ | |||||||
|     'django.contrib.messages', |     'django.contrib.messages', | ||||||
|     'django.contrib.staticfiles', |     'django.contrib.staticfiles', | ||||||
|     'collector.apps.CollectorConfig',  # main app |     'collector.apps.CollectorConfig',  # main app | ||||||
|  |     'account.apps.AccountConfig',  # account app | ||||||
|     'rest_framework', |     'rest_framework', | ||||||
|  |     'rest_framework_simplejwt', | ||||||
|  |     'django_filters', | ||||||
|  |     'drf_spectacular', | ||||||
|     "crispy_forms", |     "crispy_forms", | ||||||
|     "crispy_bootstrap5", |     "crispy_bootstrap5", | ||||||
|  |     'django_otp', | ||||||
|  |     'django_otp.plugins.otp_static', | ||||||
|  |     'django_otp.plugins.otp_totp', | ||||||
|  |     'two_factor.plugins.phonenumber',  # <- if you want phone number capability | ||||||
|  |     'two_factor', | ||||||
|     'django_cleanup.apps.CleanupConfig',  # required bottom |     'django_cleanup.apps.CleanupConfig',  # required bottom | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| @ -46,6 +57,7 @@ MIDDLEWARE = [ | |||||||
|     'django.middleware.common.CommonMiddleware', |     'django.middleware.common.CommonMiddleware', | ||||||
|     'django.middleware.csrf.CsrfViewMiddleware', |     'django.middleware.csrf.CsrfViewMiddleware', | ||||||
|     'django.contrib.auth.middleware.AuthenticationMiddleware', |     'django.contrib.auth.middleware.AuthenticationMiddleware', | ||||||
|  |     'django_otp.middleware.OTPMiddleware', | ||||||
|     'django.contrib.messages.middleware.MessageMiddleware', |     'django.contrib.messages.middleware.MessageMiddleware', | ||||||
|     'django.middleware.clickjacking.XFrameOptionsMiddleware', |     'django.middleware.clickjacking.XFrameOptionsMiddleware', | ||||||
| ] | ] | ||||||
| @ -55,7 +67,7 @@ ROOT_URLCONF = 'logs_collector.urls' | |||||||
| TEMPLATES = [ | TEMPLATES = [ | ||||||
|     { |     { | ||||||
|         'BACKEND': 'django.template.backends.django.DjangoTemplates', |         'BACKEND': 'django.template.backends.django.DjangoTemplates', | ||||||
|         'DIRS': [], |         'DIRS': [BASE_DIR / 'templates'], | ||||||
|         'APP_DIRS': True, |         'APP_DIRS': True, | ||||||
|         'OPTIONS': { |         'OPTIONS': { | ||||||
|             'context_processors': [ |             'context_processors': [ | ||||||
| @ -106,7 +118,7 @@ AUTH_PASSWORD_VALIDATORS = [ | |||||||
| 
 | 
 | ||||||
| LANGUAGE_CODE = 'en-us' | LANGUAGE_CODE = 'en-us' | ||||||
| 
 | 
 | ||||||
| TIME_ZONE = 'Asia/Irkutsk' | TIME_ZONE = env('TZ', default='UTC') | ||||||
| 
 | 
 | ||||||
| USE_I18N = True | USE_I18N = True | ||||||
| 
 | 
 | ||||||
| @ -117,6 +129,9 @@ USE_TZ = True | |||||||
| # https://docs.djangoproject.com/en/4.2/howto/static-files/ | # https://docs.djangoproject.com/en/4.2/howto/static-files/ | ||||||
| 
 | 
 | ||||||
| STATIC_URL = 'static/' | STATIC_URL = 'static/' | ||||||
|  | STATICFILES_DIRS = [ | ||||||
|  |     BASE_DIR / "static", | ||||||
|  | ] | ||||||
| 
 | 
 | ||||||
| # Default primary key field type | # Default primary key field type | ||||||
| # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field | ||||||
| @ -135,3 +150,83 @@ MEDIA_URL_FOR_SENSITIVE_FILES = '/archives/' | |||||||
| CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" | CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" | ||||||
| 
 | 
 | ||||||
| CRISPY_TEMPLATE_PACK = "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_AUTHENTICATION_CLASSES': [ | ||||||
|  |         'rest_framework_simplejwt.authentication.JWTAuthentication', | ||||||
|  |         'rest_framework.authentication.SessionAuthentication', | ||||||
|  |         'rest_framework.authentication.BasicAuthentication' | ||||||
|  |     ], | ||||||
|  |     'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend', ],  # noqa:E501 | ||||||
|  |     'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', | ||||||
|  |     # 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',  # noqa:E501 | ||||||
|  |     # 'PAGE_SIZE': 3, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | # https://drf-spectacular.readthedocs.io/en/latest/readme.html | ||||||
|  | # TODO: set environ vars config! | ||||||
|  | 
 | ||||||
|  | SPECTACULAR_SETTINGS = { | ||||||
|  |     'TITLE': 'Logs collector API', | ||||||
|  |     'DESCRIPTION': 'Collector of archives with log files for further analysis', | ||||||
|  |     'VERSION': '0.1.0', | ||||||
|  |     'SERVE_INCLUDE_SCHEMA': True, | ||||||
|  |     'SERVE_PUBLIC': False, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | # https://django-rest-framework-simplejwt.readthedocs.io/en/latest/ | ||||||
|  | 
 | ||||||
|  | SIMPLE_JWT = { | ||||||
|  |     "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), | ||||||
|  |     "REFRESH_TOKEN_LIFETIME": timedelta(days=1), | ||||||
|  |     "ROTATE_REFRESH_TOKENS": False, | ||||||
|  |     "BLACKLIST_AFTER_ROTATION": False, | ||||||
|  |     "UPDATE_LAST_LOGIN": False, | ||||||
|  | 
 | ||||||
|  |     "ALGORITHM": "HS256", | ||||||
|  |     "SIGNING_KEY": SECRET_KEY, | ||||||
|  |     "VERIFYING_KEY": "", | ||||||
|  |     "AUDIENCE": None, | ||||||
|  |     "ISSUER": None, | ||||||
|  |     "JSON_ENCODER": None, | ||||||
|  |     "JWK_URL": None, | ||||||
|  |     "LEEWAY": 0, | ||||||
|  | 
 | ||||||
|  |     "AUTH_HEADER_TYPES": ("Bearer",), | ||||||
|  |     "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", | ||||||
|  |     "USER_ID_FIELD": "id", | ||||||
|  |     "USER_ID_CLAIM": "user_id", | ||||||
|  |     "USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",  # noqa:E501 | ||||||
|  | 
 | ||||||
|  |     "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), | ||||||
|  |     "TOKEN_TYPE_CLAIM": "token_type", | ||||||
|  |     "TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser", | ||||||
|  | 
 | ||||||
|  |     "JTI_CLAIM": "jti", | ||||||
|  | 
 | ||||||
|  |     "SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp", | ||||||
|  |     "SLIDING_TOKEN_LIFETIME": timedelta(minutes=5), | ||||||
|  |     "SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1), | ||||||
|  | 
 | ||||||
|  |     "TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",  # noqa:E501 | ||||||
|  |     "TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",  # noqa:E501 | ||||||
|  |     "TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",  # noqa:E501 | ||||||
|  |     "TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",  # noqa:E501 | ||||||
|  |     "SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",  # noqa:E501 | ||||||
|  |     "SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",  # noqa:E501 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | LOGIN_URL = 'two_factor:login' | ||||||
|  | LOGIN_REDIRECT_URL = 'collector:index' | ||||||
|  | LOGOUT_REDIRECT_URL = 'two_factor:login' | ||||||
|  | |||||||
| @ -1,31 +1,62 @@ | |||||||
| """ |  | ||||||
| URL configuration for logs_collector project. |  | ||||||
| 
 |  | ||||||
| The `urlpatterns` list routes URLs to views. For more information please see: |  | ||||||
|     https://docs.djangoproject.com/en/4.2/topics/http/urls/ |  | ||||||
| Examples: |  | ||||||
| Function views |  | ||||||
|     1. Add an import:  from my_app import views |  | ||||||
|     2. Add a URL to urlpatterns:  path('', views.home, name='home') |  | ||||||
| Class-based views |  | ||||||
|     1. Add an import:  from other_app.views import Home |  | ||||||
|     2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home') |  | ||||||
| Including another URLconf |  | ||||||
|     1. Import the include() function: from django.urls import include, path |  | ||||||
|     2. Add a URL to urlpatterns:  path('blog/', include('blog.urls')) |  | ||||||
| """ |  | ||||||
| from django.conf.urls.static import static | from django.conf.urls.static import static | ||||||
| from django.contrib import admin | from django.contrib import admin | ||||||
| from django.urls import path, include | from django.urls import path, include | ||||||
| 
 | 
 | ||||||
|  | from drf_spectacular.views import ( | ||||||
|  |     SpectacularAPIView, | ||||||
|  |     SpectacularRedocView, | ||||||
|  |     SpectacularSwaggerView | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | from two_factor.urls import urlpatterns as tf_urls | ||||||
|  | 
 | ||||||
| from logs_collector import settings | from logs_collector import settings | ||||||
|  | from account.utils import AdminSiteOTPRequiredMixinRedirectSetup | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # ? 2FA patch (Admin site protection) | ||||||
|  | admin.site.__class__ = AdminSiteOTPRequiredMixinRedirectSetup | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     path('admin/', admin.site.urls), |     path( | ||||||
|     path('', include('collector.urls', namespace='collector')), |         'admin/', | ||||||
|  |         admin.site.urls | ||||||
|  |     ), | ||||||
|  |     path( | ||||||
|  |         '', | ||||||
|  |         include('collector.urls', namespace='collector') | ||||||
|  |     ), | ||||||
|  |     path( | ||||||
|  |         '', | ||||||
|  |         include(tf_urls) | ||||||
|  |     ), | ||||||
|  |     path( | ||||||
|  |         '', | ||||||
|  |         include('account.urls', namespace='account') | ||||||
|  |     ), | ||||||
|  |     path( | ||||||
|  |         'api/', | ||||||
|  |         include('collector.api.urls', namespace='collector_api') | ||||||
|  |     ), | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | # SWAGGER URLS: | ||||||
|  | urlpatterns += [ | ||||||
|  |     # API PATTERNS | ||||||
|  |     path('api/v1/schema/', SpectacularAPIView.as_view(), name='schema'), | ||||||
|  |     # Optional UI: | ||||||
|  |     path( | ||||||
|  |         'api/v1/schema/swagger/', | ||||||
|  |         SpectacularSwaggerView.as_view(url_name='schema'), | ||||||
|  |         name='swagger-ui' | ||||||
|  |     ), | ||||||
|  |     path( | ||||||
|  |         'api/v1/schema/redoc/', | ||||||
|  |         SpectacularRedocView.as_view(url_name='schema'), | ||||||
|  |         name='redoc' | ||||||
|  |     ), | ||||||
|  | ] | ||||||
| 
 | 
 | ||||||
| if settings.DEBUG: | if settings.DEBUG: | ||||||
|     urlpatterns += static( |     urlpatterns += static( | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								logs_collector/static/css/bootstrap.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										1
									
								
								logs_collector/static/css/bootstrap.min.css.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								logs_collector/static/img/android-chrome-192x192.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 7.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								logs_collector/static/img/android-chrome-512x512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 23 KiB | 
							
								
								
									
										
											BIN
										
									
								
								logs_collector/static/img/apple-touch-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								logs_collector/static/img/favicon-16x16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 495 B | 
							
								
								
									
										
											BIN
										
									
								
								logs_collector/static/img/favicon-32x32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 930 B | 
							
								
								
									
										
											BIN
										
									
								
								logs_collector/static/img/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 15 KiB | 
							
								
								
									
										1
									
								
								logs_collector/static/img/site.webmanifest
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1 @@ | |||||||
|  | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} | ||||||
							
								
								
									
										7
									
								
								logs_collector/static/js/bootstrap.bundle.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										1
									
								
								logs_collector/static/js/bootstrap.bundle.min.js.map
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										90
									
								
								logs_collector/static/js/bs.theme.mode.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,90 @@ | |||||||
|  | /*! | ||||||
|  |  * Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
 | ||||||
|  |  * Copyright 2011-2023 The Bootstrap Authors | ||||||
|  |  * Licensed under the Creative Commons Attribution 3.0 Unported License. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | ;(() => { | ||||||
|  |   "use strict" | ||||||
|  | 
 | ||||||
|  |   const getStoredTheme = () => localStorage.getItem("theme") | ||||||
|  |   const setStoredTheme = (theme) => localStorage.setItem("theme", theme) | ||||||
|  | 
 | ||||||
|  |   const getPreferredTheme = () => { | ||||||
|  |     const storedTheme = getStoredTheme() | ||||||
|  |     if (storedTheme) { | ||||||
|  |       return storedTheme | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return window.matchMedia("(prefers-color-scheme: dark)").matches | ||||||
|  |       ? "dark" | ||||||
|  |       : "light" | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const setTheme = (theme) => { | ||||||
|  |     if ( | ||||||
|  |       theme === "auto" && | ||||||
|  |       window.matchMedia("(prefers-color-scheme: dark)").matches | ||||||
|  |     ) { | ||||||
|  |       document.documentElement.setAttribute("data-bs-theme", "dark") | ||||||
|  |     } else { | ||||||
|  |       document.documentElement.setAttribute("data-bs-theme", theme) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   setTheme(getPreferredTheme()) | ||||||
|  | 
 | ||||||
|  |   const showActiveTheme = (theme, focus = false) => { | ||||||
|  |     const themeSwitcher = document.querySelector("#bd-theme") | ||||||
|  | 
 | ||||||
|  |     if (!themeSwitcher) { | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const themeSwitcherText = document.querySelector("#bd-theme-text") | ||||||
|  |     const activeThemeIcon = document.querySelector(".theme-icon-active use") | ||||||
|  |     const btnToActive = document.querySelector( | ||||||
|  |       `[data-bs-theme-value="${theme}"]` | ||||||
|  |     ) | ||||||
|  |     const svgOfActiveBtn = btnToActive | ||||||
|  |       .querySelector("svg use") | ||||||
|  |       .getAttribute("href") | ||||||
|  | 
 | ||||||
|  |     document.querySelectorAll("[data-bs-theme-value]").forEach((element) => { | ||||||
|  |       element.classList.remove("active") | ||||||
|  |       element.setAttribute("aria-pressed", "false") | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     btnToActive.classList.add("active") | ||||||
|  |     btnToActive.setAttribute("aria-pressed", "true") | ||||||
|  |     activeThemeIcon.setAttribute("href", svgOfActiveBtn) | ||||||
|  |     const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})` | ||||||
|  |     themeSwitcher.setAttribute("aria-label", themeSwitcherLabel) | ||||||
|  | 
 | ||||||
|  |     if (focus) { | ||||||
|  |       themeSwitcher.focus() | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   window | ||||||
|  |     .matchMedia("(prefers-color-scheme: dark)") | ||||||
|  |     .addEventListener("change", () => { | ||||||
|  |       const storedTheme = getStoredTheme() | ||||||
|  |       if (storedTheme !== "light" && storedTheme !== "dark") { | ||||||
|  |         setTheme(getPreferredTheme()) | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |   window.addEventListener("DOMContentLoaded", () => { | ||||||
|  |     showActiveTheme(getPreferredTheme()) | ||||||
|  | 
 | ||||||
|  |     document.querySelectorAll("[data-bs-theme-value]").forEach((toggle) => { | ||||||
|  |       toggle.addEventListener("click", () => { | ||||||
|  |         const theme = toggle.getAttribute("data-bs-theme-value") | ||||||
|  |         setStoredTheme(theme) | ||||||
|  |         setTheme(theme) | ||||||
|  |         showActiveTheme(theme, true) | ||||||
|  |       }) | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | })() | ||||||
							
								
								
									
										6
									
								
								logs_collector/static/js/bs.tooltip.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,6 @@ | |||||||
|  | const tooltipTriggerList = document.querySelectorAll( | ||||||
|  |     '[data-bs-toggle="tooltip"]' | ||||||
|  | ) | ||||||
|  | const tooltipList = [...tooltipTriggerList].map( | ||||||
|  |     (tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl) | ||||||
|  | ) | ||||||
							
								
								
									
										50
									
								
								logs_collector/templates/base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,50 @@ | |||||||
|  | {% load static %} | ||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en" data-bs-theme="auto"> | ||||||
|  |   <head> | ||||||
|  |     <meta charset="UTF-8" /> | ||||||
|  |     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||||
|  |     <link | ||||||
|  |       rel="stylesheet" | ||||||
|  |       href="{% static '/css/bootstrap.min.css' %}" | ||||||
|  |       rel="stylesheet" | ||||||
|  |     > | ||||||
|  |     <link | ||||||
|  |       rel="apple-touch-icon" | ||||||
|  |       sizes="180x180" | ||||||
|  |       href="{% static '/img/apple-touch-icon.png' %}" | ||||||
|  |     > | ||||||
|  |     <link | ||||||
|  |       rel="icon" | ||||||
|  |       type="image/png" | ||||||
|  |       sizes="32x32" | ||||||
|  |       href="{% static '/img/favicon-32x32.png' %}" | ||||||
|  |     > | ||||||
|  |     <link | ||||||
|  |       rel="icon" | ||||||
|  |       type="image/png" | ||||||
|  |       sizes="16x16" | ||||||
|  |       href="{% static '/img/favicon-16x16.png' %}" | ||||||
|  |     > | ||||||
|  |     <link  | ||||||
|  |       rel="manifest" | ||||||
|  |       href="{% static '/img/site.webmanifest' %}" | ||||||
|  |     > | ||||||
|  |     <link  | ||||||
|  |     rel="stylesheet" | ||||||
|  |     href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" | ||||||
|  |     > | ||||||
|  |     {% block collector_head %}{% endblock collector_head %} | ||||||
|  |     {% block account_head %}{% endblock account_head %} | ||||||
|  |   </head> | ||||||
|  |   <body> | ||||||
|  |     {% block collector_content %}{% endblock collector_content %} | ||||||
|  |     {% block account_content %}{% endblock account_content %} | ||||||
|  |      | ||||||
|  |     <script src="{% static '/js/bootstrap.bundle.min.js' %}"></script> | ||||||
|  |     <script src="{% static '/js/bs.theme.mode.js' %}"></script> | ||||||
|  |      | ||||||
|  |     {% block collector_scripts %}{% endblock collector_scripts %} | ||||||
|  |     {% block account_scripts %}{% endblock account_scripts %} | ||||||
|  |   </body> | ||||||
|  | </html> | ||||||
| @ -1,20 +1,5 @@ | |||||||
| {% load collector_extras %} | {% load collector_extras %} | ||||||
| {% get_platforms as platforms %} | {% get_platforms as platforms %} | ||||||
| <svg xmlns="http://www.w3.org/2000/svg" class="d-none"> |  | ||||||
|   <symbol id="check2" viewBox="0 0 16 16"> |  | ||||||
|     <path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/> |  | ||||||
|   </symbol> |  | ||||||
|   <symbol id="circle-half" fill="currentColor" viewBox="0 0 16 16"> |  | ||||||
|     <path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/> |  | ||||||
|   </symbol> |  | ||||||
|   <symbol id="moon-stars-fill" fill="currentColor" viewBox="0 0 16 16"> |  | ||||||
|     <path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/> |  | ||||||
|     <path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"/> |  | ||||||
|   </symbol> |  | ||||||
|   <symbol id="sun-fill" fill="currentColor" viewBox="0 0 16 16"> |  | ||||||
|     <path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/> |  | ||||||
|   </symbol> |  | ||||||
| </svg> |  | ||||||
| <nav class="navbar navbar-expand-lg bg-body-tertiary"> | <nav class="navbar navbar-expand-lg bg-body-tertiary"> | ||||||
|   <div class="container"> |   <div class="container"> | ||||||
|     <a  |     <a  | ||||||
| @ -105,12 +90,29 @@ | |||||||
|               {% if request.user.is_staff %} |               {% if request.user.is_staff %} | ||||||
|               <li> |               <li> | ||||||
|                 <a class="dropdown-item" type="button" href="{% url 'admin:index' %}" |                 <a class="dropdown-item" type="button" href="{% url 'admin:index' %}" | ||||||
|                 ><i class="bi bi-shield-shaded"></i></i> Admin</a> |                 ><i class="bi bi-shield-shaded"></i> Admin</a> | ||||||
|               </li> |               </li> | ||||||
|               {% endif %} |               {% endif %} | ||||||
|               <li><button class="dropdown-item" type="button"><i class="bi bi-gear"></i> Settings</button></li> |               <li> | ||||||
|  |                 <a class="dropdown-item" type="button" href="{% url 'swagger-ui' %}" | ||||||
|  |                 ><i class="bi bi-braces-asterisk"></i> Swagger</a> | ||||||
|  |               </li> | ||||||
|  |               <li> | ||||||
|  |                 <a | ||||||
|  |                   href="{% url 'two_factor:profile' %}" | ||||||
|  |                   class="dropdown-item" | ||||||
|  |                   type="button"> | ||||||
|  |                     <i class="bi bi-gear"></i> Settings | ||||||
|  |                 </a> | ||||||
|  |               </li> | ||||||
|               <li><hr class="dropdown-divider" /></li> |               <li><hr class="dropdown-divider" /></li> | ||||||
|               <li><button class="dropdown-item" type="button"><i class="bi bi-door-closed"></i> Logout</button></li>            |               <li> | ||||||
|  |                 <a | ||||||
|  |                   href="{% url 'account:logout' %}" | ||||||
|  |                   class="dropdown-item" | ||||||
|  |                   type="button"><i class="bi bi-door-closed"></i> Logout | ||||||
|  |                 </a> | ||||||
|  |               </li>            | ||||||
|             </ul> |             </ul> | ||||||
|           </div>               |           </div>               | ||||||
|         </li> |         </li> | ||||||
| @ -119,7 +121,7 @@ | |||||||
|           <div class="btn-group"> |           <div class="btn-group"> | ||||||
|             <a |             <a | ||||||
|               type="button" |               type="button" | ||||||
|               href="{% url 'admin:index' %}" |               href="{% url 'two_factor:login' %}" | ||||||
|               class="btn btn-outline-secondary" |               class="btn btn-outline-secondary" | ||||||
|             > |             > | ||||||
|               <i class="bi bi-box-arrow-in-right"></i></i> Login |               <i class="bi bi-box-arrow-in-right"></i></i> Login | ||||||
| @ -135,42 +137,7 @@ | |||||||
|         <!-- Theme switcher--> |         <!-- Theme switcher--> | ||||||
|         <li class="nav-item dropdown"> |         <li class="nav-item dropdown"> | ||||||
|           <div class="dropdown bd-mode-toggle"> |           <div class="dropdown bd-mode-toggle"> | ||||||
|             <button class="btn btn-bd-primary py-2 dropdown-toggle d-flex align-items-center" |             {% include 'includes/theme_switcher.html' %} | ||||||
|                     id="bd-theme" |  | ||||||
|                     type="button" |  | ||||||
|                     aria-expanded="false" |  | ||||||
|                     data-bs-toggle="dropdown" |  | ||||||
|                     aria-label="Toggle theme (auto)"> |  | ||||||
|               <svg class="bi my-1 theme-icon-active" width="1em" height="1em"><use href="#circle-half"></use></svg> |  | ||||||
|               <span class="visually-hidden" id="bd-theme-text">Toggle theme</span> |  | ||||||
|             </button> |  | ||||||
|             <ul class="dropdown-menu dropdown-menu-end shadow" aria-labelledby="bd-theme-text"> |  | ||||||
|               <li> |  | ||||||
|                 <button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light" aria-pressed="false"> |  | ||||||
|                   <svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"><use href="#sun-fill"></use></svg> |  | ||||||
|                   Light |  | ||||||
|                   <svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg> |  | ||||||
|                 </button> |  | ||||||
|               </li> |  | ||||||
|               <li> |  | ||||||
|                 <button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark" aria-pressed="false"> |  | ||||||
|                   <svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"><use href="#moon-stars-fill"></use></svg> |  | ||||||
|                   Dark |  | ||||||
|                   <svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg> |  | ||||||
|                 </button> |  | ||||||
|               </li> |  | ||||||
|               <li> |  | ||||||
|                 <button |  | ||||||
|                   type="button" |  | ||||||
|                   class="dropdown-item d-flex align-items-center active" |  | ||||||
|                   data-bs-theme-value="auto" aria-pressed="true" |  | ||||||
|                 > |  | ||||||
|                   <svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"><use href="#circle-half"></use></svg> |  | ||||||
|                   Auto |  | ||||||
|                   <svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg> |  | ||||||
|                 </button> |  | ||||||
|               </li> |  | ||||||
|             </ul> |  | ||||||
|           </div> |           </div> | ||||||
|         </li> |         </li> | ||||||
|       </ul> |       </ul> | ||||||
							
								
								
									
										91
									
								
								logs_collector/templates/includes/theme_switcher.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,91 @@ | |||||||
|  | <!--Theme switcher icons--> | ||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" class="d-none"> | ||||||
|  |   <symbol id="check2" viewBox="0 0 16 16"> | ||||||
|  |     <path | ||||||
|  |       d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" | ||||||
|  |     /> | ||||||
|  |   </symbol> | ||||||
|  |   <symbol id="circle-half" fill="currentColor" viewBox="0 0 16 16"> | ||||||
|  |     <path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z" /> | ||||||
|  |   </symbol> | ||||||
|  |   <symbol id="moon-stars-fill" fill="currentColor" viewBox="0 0 16 16"> | ||||||
|  |     <path | ||||||
|  |       d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z" | ||||||
|  |     /> | ||||||
|  |     <path | ||||||
|  |       d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z" | ||||||
|  |     /> | ||||||
|  |   </symbol> | ||||||
|  |   <symbol id="sun-fill" fill="currentColor" viewBox="0 0 16 16"> | ||||||
|  |     <path | ||||||
|  |       d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" | ||||||
|  |     /> | ||||||
|  |   </symbol> | ||||||
|  | </svg> | ||||||
|  | <!-- Theme switcher dropdown buttons --> | ||||||
|  | <button | ||||||
|  |   class="btn btn-bd-primary py-2 dropdown-toggle d-flex align-items-center" | ||||||
|  |   id="bd-theme" | ||||||
|  |   type="button" | ||||||
|  |   aria-expanded="false" | ||||||
|  |   data-bs-toggle="dropdown" | ||||||
|  |   aria-label="Toggle theme (auto)" | ||||||
|  | > | ||||||
|  |   <svg class="bi my-1 theme-icon-active" width="1em" height="1em"> | ||||||
|  |     <use href="#circle-half"></use> | ||||||
|  |   </svg> | ||||||
|  |   <span class="visually-hidden" id="bd-theme-text">Toggle theme</span> | ||||||
|  | </button> | ||||||
|  | <ul | ||||||
|  |   class="dropdown-menu dropdown-menu-end shadow" | ||||||
|  |   aria-labelledby="bd-theme-text" | ||||||
|  | > | ||||||
|  |   <li> | ||||||
|  |     <button | ||||||
|  |       type="button" | ||||||
|  |       class="dropdown-item d-flex align-items-center" | ||||||
|  |       data-bs-theme-value="light" | ||||||
|  |       aria-pressed="false" | ||||||
|  |     > | ||||||
|  |       <svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"> | ||||||
|  |         <use href="#sun-fill"></use> | ||||||
|  |       </svg> | ||||||
|  |       Light | ||||||
|  |       <svg class="bi ms-auto d-none" width="1em" height="1em"> | ||||||
|  |         <use href="#check2"></use> | ||||||
|  |       </svg> | ||||||
|  |     </button> | ||||||
|  |   </li> | ||||||
|  |   <li> | ||||||
|  |     <button | ||||||
|  |       type="button" | ||||||
|  |       class="dropdown-item d-flex align-items-center" | ||||||
|  |       data-bs-theme-value="dark" | ||||||
|  |       aria-pressed="false" | ||||||
|  |     > | ||||||
|  |       <svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"> | ||||||
|  |         <use href="#moon-stars-fill"></use> | ||||||
|  |       </svg> | ||||||
|  |       Dark | ||||||
|  |       <svg class="bi ms-auto d-none" width="1em" height="1em"> | ||||||
|  |         <use href="#check2"></use> | ||||||
|  |       </svg> | ||||||
|  |     </button> | ||||||
|  |   </li> | ||||||
|  |   <li> | ||||||
|  |     <button | ||||||
|  |       type="button" | ||||||
|  |       class="dropdown-item d-flex align-items-center active" | ||||||
|  |       data-bs-theme-value="auto" | ||||||
|  |       aria-pressed="true" | ||||||
|  |     > | ||||||
|  |       <svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"> | ||||||
|  |         <use href="#circle-half"></use> | ||||||
|  |       </svg> | ||||||
|  |       Auto | ||||||
|  |       <svg class="bi ms-auto d-none" width="1em" height="1em"> | ||||||
|  |         <use href="#check2"></use> | ||||||
|  |       </svg> | ||||||
|  |     </button> | ||||||
|  |   </li> | ||||||
|  | </ul> | ||||||
| After Width: | Height: | Size: 3.9 KiB | 
							
								
								
									
										18
									
								
								logs_collector/templates/two_factor/_base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,18 @@ | |||||||
|  | {% extends 'base.html' %} | ||||||
|  | {% load static %} | ||||||
|  | 
 | ||||||
|  | {% block account_head %} | ||||||
|  |   <title>Collector - {% block title %}{% endblock %}</title> | ||||||
|  |   {% block extra_media %}{% endblock %} | ||||||
|  | {% endblock account_head %} | ||||||
|  | 
 | ||||||
|  | {% block account_content %} | ||||||
|  |   {% block content_wrapper %} | ||||||
|  |     {% block content %}{% endblock %} | ||||||
|  |   {% endblock %} | ||||||
|  | <section> | ||||||
|  |   <footer> | ||||||
|  |     {% block theme_switcher %}{% endblock %} | ||||||
|  |   </footer> | ||||||
|  | </section> | ||||||
|  | {% endblock account_content %} | ||||||
							
								
								
									
										22
									
								
								logs_collector/templates/two_factor/_base_focus.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,22 @@ | |||||||
|  | {% extends "two_factor/_base.html" %} | ||||||
|  | 
 | ||||||
|  | {% block content_wrapper %} | ||||||
|  |   <section> | ||||||
|  |     <header> | ||||||
|  |       {% block nav %}{% endblock nav %} | ||||||
|  |     </header> | ||||||
|  |   </section> | ||||||
|  |   <section> | ||||||
|  |     <main> | ||||||
|  |       <div class="container mt-3"> | ||||||
|  |         <div class="card"> | ||||||
|  |           <div class="card-body"> | ||||||
|  |             <div class="text-center"> | ||||||
|  |               {% block content %}{% endblock %} | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </main> | ||||||
|  |   </section> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										16
									
								
								logs_collector/templates/two_factor/_wizard_actions.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,16 @@ | |||||||
|  | {% load i18n %} | ||||||
|  | 
 | ||||||
|  | {% if cancel_url %} | ||||||
|  |   <a href="{{ cancel_url }}" class="float-right btn btn-link">{% trans "Cancel" %}</a> | ||||||
|  | {% endif %} | ||||||
|  | {% if wizard.steps.prev %} | ||||||
|  |   <button  | ||||||
|  |     name="wizard_goto_step" | ||||||
|  |     type="submit" | ||||||
|  |     value="{{ wizard.steps.prev }}" | ||||||
|  |     class="btn btn-secondary">{% trans "Back" %} | ||||||
|  |   </button> | ||||||
|  | {% else %} | ||||||
|  |   <button disabled name="" type="button" class="btn">{% trans "Back" %}</button> | ||||||
|  | {% endif %} | ||||||
|  | <button type="submit" class="btn btn-primary">{% trans "Next" %}</button> | ||||||
							
								
								
									
										5
									
								
								logs_collector/templates/two_factor/_wizard_forms.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,5 @@ | |||||||
|  | {% load crispy_forms_tags %} | ||||||
|  | <div class="mb-3"> | ||||||
|  |   {{ wizard.management_form }} | ||||||
|  |   {{ wizard.form|crispy }} | ||||||
|  | </div> | ||||||
							
								
								
									
										35
									
								
								logs_collector/templates/two_factor/core/backup_tokens.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,35 @@ | |||||||
|  | {% extends "two_factor/_base_focus.html" %} | ||||||
|  | {% load i18n %} | ||||||
|  | {% load crispy_forms_tags %} | ||||||
|  | 
 | ||||||
|  | {% block nav %}{% include 'includes/navigation.html' %}{% endblock nav %} | ||||||
|  | 
 | ||||||
|  | {% block content %} | ||||||
|  | 
 | ||||||
|  |   <h1>{% block title %}{% trans "Backup Tokens" %}{% endblock %}</h1> | ||||||
|  |   <div class="mb-3 d-flex justify-content-center"> | ||||||
|  |     <p>{% blocktrans trimmed %}Backup tokens can be used when your primary and backup | ||||||
|  |         phone numbers aren't available. The backup tokens below can be used | ||||||
|  |         for login verification. If you've used up all your backup tokens, you | ||||||
|  |         can generate a new set of backup tokens. Only the backup tokens shown | ||||||
|  |         below will be valid.{% endblocktrans %}</p> | ||||||
|  |   </div> | ||||||
|  |   {% if device.token_set.count %} | ||||||
|  |     <ul class="list-group"> | ||||||
|  |       {% for token in device.token_set.all %} | ||||||
|  |         <li class="list-group-item list-group-item-action">{{ token.token }}</li> | ||||||
|  |       {% endfor %} | ||||||
|  |     </ul> | ||||||
|  |     <p>{% blocktrans %}Print these tokens and keep them somewhere safe.{% endblocktrans %}</p> | ||||||
|  |   {% else %} | ||||||
|  |     <p>{% trans "You don't have any backup codes yet." %}</p> | ||||||
|  |   {% endif %} | ||||||
|  | 
 | ||||||
|  |   <form method="post">{% csrf_token %}{{ form|crispy }} | ||||||
|  |     <a  | ||||||
|  |       href="{% url 'two_factor:profile'%}" | ||||||
|  |       class="float-right btn btn-link">{% trans "Back to Account Security" %} | ||||||
|  |     </a> | ||||||
|  |     <button class="btn btn-outline-primary" type="submit">{% trans "Generate Tokens" %}</button> | ||||||
|  |   </form> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										71
									
								
								logs_collector/templates/two_factor/core/login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,71 @@ | |||||||
|  | {% extends "two_factor/_base.html" %} | ||||||
|  | {% load i18n %} | ||||||
|  | {% load two_factor_tags %} | ||||||
|  | 
 | ||||||
|  | {% block extra_media %} | ||||||
|  |   {{ form.media }} | ||||||
|  | {% endblock %} | ||||||
|  | 
 | ||||||
|  | {% block content %} | ||||||
|  | <div class="d-flex min-vh-100 align-items-center py-4" cz-shortcut-listen="true"> | ||||||
|  |   <div class="container"> | ||||||
|  |     <div class="row"> | ||||||
|  |       <div class="col-md-3 offset-md-4"> | ||||||
|  |         <h1>{% block title %}{% trans "Login" %}{% endblock %}</h1> | ||||||
|  |         {% if wizard.steps.current == 'auth' %} | ||||||
|  |           <p>{% blocktrans %}Enter your credentials.{% endblocktrans %}</p> | ||||||
|  |         {% elif wizard.steps.current == 'token' %} | ||||||
|  |           <p>{{ device|as_verbose_action }}</p> | ||||||
|  |         {% elif wizard.steps.current == 'backup' %} | ||||||
|  |           <p>{% blocktrans trimmed %}Use this form for entering backup tokens for logging in. | ||||||
|  |             These tokens have been generated for you to print and keep safe. Please | ||||||
|  |             enter one of these backup tokens to login to your account.{% endblocktrans %}</p> | ||||||
|  |         {% endif %} | ||||||
|  |        | ||||||
|  |         <form action="" method="post">{% csrf_token %} | ||||||
|  |           {% include "two_factor/_wizard_forms.html" %} | ||||||
|  |        | ||||||
|  |           {# hidden submit button to enable [enter] key #} | ||||||
|  |           <input type="submit" value="" hidden /> | ||||||
|  |        | ||||||
|  |           {% if other_devices %} | ||||||
|  |             <p>{% trans "Or, alternatively, use one of your other authentication methods:" %}</p> | ||||||
|  |             <p> | ||||||
|  |               {% for other in other_devices %} | ||||||
|  |               <button name="challenge_device" value="{{ other.persistent_id }}" | ||||||
|  |                       class="btn btn-secondary btn-block" type="submit"> | ||||||
|  |                 {{ other|as_action }} | ||||||
|  |               </button> | ||||||
|  |             {% endfor %}</p> | ||||||
|  |           {% endif %} | ||||||
|  |        | ||||||
|  |           {% include "two_factor/_wizard_actions.html" %} | ||||||
|  |         </form> | ||||||
|  |        | ||||||
|  |         {% block 'backup_tokens' %} | ||||||
|  |           {% if backup_tokens %} | ||||||
|  |             <hr> | ||||||
|  |             <div class="backup_tokens_form"> | ||||||
|  |             <form action="" method="post"> | ||||||
|  |                 {% csrf_token %} | ||||||
|  |                   <p>{% trans "As a last resort, you can use a backup token:" %}</p> | ||||||
|  |                   <p> | ||||||
|  |                       <button name="wizard_goto_step" type="submit" value="backup" | ||||||
|  |                           class="btn btn-sm btn-secondary btn-block">{% trans "Use Backup Token" %}</button> | ||||||
|  |                   </p> | ||||||
|  |             </form> | ||||||
|  |             </div> | ||||||
|  |           {% endif %} | ||||||
|  |         {% endblock %} | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
|  | 
 | ||||||
|  | <!-- Theme switcher --> | ||||||
|  | {% block theme_switcher %} | ||||||
|  | <div class="dropdown position-fixed bottom-0 end-0 mb-3 me-3 bd-mode-toggle"> | ||||||
|  |   {% include 'includes/theme_switcher.html' %} | ||||||
|  | </div> | ||||||
|  | {% endblock theme_switcher %} | ||||||
							
								
								
									
										35
									
								
								logs_collector/templates/two_factor/core/otp_required.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,35 @@ | |||||||
|  | {% extends "two_factor/_base.html" %} | ||||||
|  | {% load i18n %} | ||||||
|  | 
 | ||||||
|  | {% block content %} | ||||||
|  | <div class="d-flex min-vh-100 align-items-center py-4" cz-shortcut-listen="true"> | ||||||
|  |   <div class="container"> | ||||||
|  |     <div class="row"> | ||||||
|  |       <div class="col-md-4 offset-md-4"> | ||||||
|  |         <h1>{% block title %}{% trans "Permission Denied" %}{% endblock %}</h1> | ||||||
|  | 
 | ||||||
|  |         <p>{% blocktrans trimmed %}The page you requested, enforces users to verify using | ||||||
|  |           two-factor authentication for security reasons. You need to enable these | ||||||
|  |           security features in order to access this page.{% endblocktrans %}</p> | ||||||
|  |            | ||||||
|  |         <p>{% blocktrans trimmed %}Two-factor authentication is not enabled for your | ||||||
|  |           account. Enable two-factor authentication for enhanced account | ||||||
|  |           security.{% endblocktrans %}</p> | ||||||
|  |         <p> | ||||||
|  |           <a href="javascript:history.go(-1)" | ||||||
|  |             class="float-right btn btn-link">{% trans "Go back" %}</a> | ||||||
|  |           <a href="{% url 'two_factor:setup' %}" class="btn btn-primary"> | ||||||
|  |           {% trans "Enable Two-Factor Authentication" %}</a> | ||||||
|  |         </p> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | {% endblock %} | ||||||
|  | 
 | ||||||
|  | <!-- Theme switcher --> | ||||||
|  | {% block theme_switcher %} | ||||||
|  | <div class="dropdown position-fixed bottom-0 end-0 mb-3 me-3 bd-mode-toggle"> | ||||||
|  |   {% include 'includes/theme_switcher.html' %} | ||||||
|  | </div> | ||||||
|  | {% endblock theme_switcher %} | ||||||
							
								
								
									
										24
									
								
								logs_collector/templates/two_factor/core/phone_register.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,24 @@ | |||||||
|  | {% extends "two_factor/_base_focus.html" %} | ||||||
|  | {% load i18n %} | ||||||
|  | 
 | ||||||
|  | {% block content %} | ||||||
|  |   <h1>{% block title %}{% trans "Add Backup Phone" %}{% endblock %}</h1> | ||||||
|  | 
 | ||||||
|  |   {% if wizard.steps.current == 'setup' %} | ||||||
|  |       <p>{% blocktrans trimmed %}You'll be adding a backup phone number to your | ||||||
|  |         account. This number will be used if your primary method of | ||||||
|  |         registration is not available.{% endblocktrans %}</p> | ||||||
|  |   {% elif wizard.steps.current == 'validation' %} | ||||||
|  |       <p>{% blocktrans trimmed %}We've sent a token to your phone number. Please | ||||||
|  |         enter the token you've received.{% endblocktrans %}</p> | ||||||
|  |   {% endif %} | ||||||
|  | 
 | ||||||
|  |   <form action="" method="post">{% csrf_token %} | ||||||
|  |     {% include "two_factor/_wizard_forms.html" %} | ||||||
|  | 
 | ||||||
|  |     {# hidden submit button to enable [enter] key #} | ||||||
|  |     <input type="submit" value="" hidden /> | ||||||
|  | 
 | ||||||
|  |     {% include "two_factor/_wizard_actions.html" %} | ||||||
|  |   </form> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										67
									
								
								logs_collector/templates/two_factor/core/setup.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,67 @@ | |||||||
|  | {% extends "two_factor/_base_focus.html" %} | ||||||
|  | {% load i18n %} | ||||||
|  | 
 | ||||||
|  | {% block extra_media %} | ||||||
|  |   {{ form.media }} | ||||||
|  | {% endblock %} | ||||||
|  | 
 | ||||||
|  | {% block nav %}{% include 'includes/navigation.html' %}{% endblock nav %} | ||||||
|  | 
 | ||||||
|  | {% block content %} | ||||||
|  |   <h1>{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1> | ||||||
|  |   {% if wizard.steps.current == 'welcome' %} | ||||||
|  |     <p>{% blocktrans trimmed %}You are about to take your account security to the | ||||||
|  |         next level. Follow the steps in this wizard to enable two-factor | ||||||
|  |         authentication.{% endblocktrans %}</p> | ||||||
|  |   {% elif wizard.steps.current == 'method' %} | ||||||
|  |     <p>{% blocktrans trimmed %}Please select which authentication method you would | ||||||
|  |         like to use.{% endblocktrans %}</p> | ||||||
|  |   {% elif wizard.steps.current == 'generator' %} | ||||||
|  |     <p>{% blocktrans trimmed %}To start using a token generator, please use your | ||||||
|  |         smartphone to scan the QR code below. For example, use Google | ||||||
|  |         Authenticator.{% endblocktrans %}</p> | ||||||
|  |     <p><img src="{{ QR_URL }}" alt="QR Code" class="bg-white"/></p> | ||||||
|  |     <p>{% blocktrans trimmed %}Alternatively you can use the following secret to | ||||||
|  |         setup TOTP in your authenticator or password manager manually.{% endblocktrans %}</p> | ||||||
|  |     <p>{% translate "TOTP Secret:" %} <a href="{{ otpauth_url }}">{{ secret_key }}</a></p> | ||||||
|  |     <p>{% blocktrans %}Then, enter the token generated by the app.{% endblocktrans %}</p> | ||||||
|  | 
 | ||||||
|  |   {% elif wizard.steps.current == 'sms' %} | ||||||
|  |     <p>{% blocktrans trimmed %}Please enter the phone number you wish to receive the | ||||||
|  |       text messages on. This number will be validated in the next step. | ||||||
|  |       {% endblocktrans %}</p> | ||||||
|  |   {% elif wizard.steps.current == 'call' %} | ||||||
|  |     <p>{% blocktrans trimmed %}Please enter the phone number you wish to be called on. | ||||||
|  |       This number will be validated in the next step. {% endblocktrans %}</p> | ||||||
|  |   {% elif wizard.steps.current == 'validation' %} | ||||||
|  |     {% if challenge_succeeded %} | ||||||
|  |       {% if device.method == 'call' %} | ||||||
|  |         <p>{% blocktrans trimmed %}We are calling your phone right now, please enter the | ||||||
|  |           digits you hear.{% endblocktrans %}</p> | ||||||
|  |       {% elif device.method == 'sms' %} | ||||||
|  |         <p>{% blocktrans trimmed %}We sent you a text message, please enter the tokens we | ||||||
|  |           sent.{% endblocktrans %}</p> | ||||||
|  |       {% endif %} | ||||||
|  |     {% else %} | ||||||
|  |       <p class="alert alert-warning" role="alert">{% blocktrans trimmed %}We've | ||||||
|  |         encountered an issue with the selected authentication method. Please | ||||||
|  |         go back and verify that you entered your information correctly, try | ||||||
|  |         again, or use a different authentication method instead. If the issue | ||||||
|  |         persists, contact the site administrator.{% endblocktrans %}</p> | ||||||
|  |     {% endif %} | ||||||
|  |   {% elif wizard.steps.current == 'yubikey' %} | ||||||
|  |     <p>{% blocktrans trimmed %}To identify and verify your YubiKey, please insert a | ||||||
|  |       token in the field below. Your YubiKey will be linked to your | ||||||
|  |       account.{% endblocktrans %}</p> | ||||||
|  |   {% endif %} | ||||||
|  |   <div class="input-group mb-3 d-flex justify-content-center"> | ||||||
|  |     <form action="" method="post">{% csrf_token %} | ||||||
|  |       {% include "two_factor/_wizard_forms.html" %} | ||||||
|  | 
 | ||||||
|  |       {# hidden submit button to enable [enter] key #} | ||||||
|  |       <input type="submit" value="" hidden /> | ||||||
|  | 
 | ||||||
|  |       {% include "two_factor/_wizard_actions.html" %} | ||||||
|  |     </form> | ||||||
|  |   </div> | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										26
									
								
								logs_collector/templates/two_factor/core/setup_complete.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,26 @@ | |||||||
|  | {% extends "two_factor/_base_focus.html" %} | ||||||
|  | {% load i18n %} | ||||||
|  | 
 | ||||||
|  | {% block nav %}{% include 'includes/navigation.html' %}{% endblock nav %} | ||||||
|  | 
 | ||||||
|  | {% block content %} | ||||||
|  |   <h1>{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1> | ||||||
|  | 
 | ||||||
|  |   <p>{% blocktrans trimmed %}Congratulations, you've successfully enabled two-factor | ||||||
|  |       authentication.{% endblocktrans %}</p> | ||||||
|  | 
 | ||||||
|  |   {% if not phone_methods %} | ||||||
|  |     <p><a href="{% url 'two_factor:profile' %}" | ||||||
|  |         class="btn btn-block btn-secondary">{% trans "Back to Account Security" %}</a></p> | ||||||
|  |   {% else %} | ||||||
|  |     <p>{% blocktrans trimmed %}However, it might happen that you don't have access to | ||||||
|  |       your primary token device. To enable account recovery, add a phone | ||||||
|  |       number.{% endblocktrans %}</p> | ||||||
|  | 
 | ||||||
|  |     <a href="{% url 'two_factor:profile' %}" | ||||||
|  |         class="float-right btn btn-link">{% trans "Back to Account Security" %}</a> | ||||||
|  |     <p><a href="{% url 'two_factor:phone_create' %}" | ||||||
|  |         class="btn btn-success">{% trans "Add Phone Number" %}</a></p> | ||||||
|  |   {% endif %} | ||||||
|  | 
 | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										19
									
								
								logs_collector/templates/two_factor/profile/disable.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,19 @@ | |||||||
|  | {% extends "two_factor/_base_focus.html" %} | ||||||
|  | {% load i18n %} | ||||||
|  | {% load crispy_forms_tags %} | ||||||
|  | 
 | ||||||
|  | {% block nav %}{% include 'includes/navigation.html' %}{% endblock nav %} | ||||||
|  | 
 | ||||||
|  | {% block content %} | ||||||
|  | <h1>{% block title %}{% trans "Disable Two-factor Authentication" %}{% endblock %}</h1> | ||||||
|  | <p>{% blocktrans trimmed %}You are about to disable two-factor authentication. This | ||||||
|  |   weakens your account security, are you sure?{% endblocktrans %}</p> | ||||||
|  | <div class="input-group mb-3 d-flex justify-content-center"> | ||||||
|  |   <form method="post"> | ||||||
|  |     {% csrf_token %} | ||||||
|  |     <div>{{ form|crispy }}</div> | ||||||
|  |     <button class="btn btn-danger" type="submit">{% trans "Disable" %}</button> | ||||||
|  |   </form> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | {% endblock %} | ||||||
							
								
								
									
										59
									
								
								logs_collector/templates/two_factor/profile/profile.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,59 @@ | |||||||
|  | {% extends "two_factor/_base_focus.html" %} | ||||||
|  | {% load i18n %} | ||||||
|  | {% load two_factor_tags %} | ||||||
|  | 
 | ||||||
|  | {% block nav %}{% include 'includes/navigation.html' %}{% endblock nav %} | ||||||
|  | 
 | ||||||
|  | {% block content %} | ||||||
|  |       <h1>{% block title %}{% trans "Account Security" %}{% endblock %}</h1> | ||||||
|  |       {% if default_device %} | ||||||
|  |         <p>{% blocktrans with primary=default_device|as_action %}Primary method: {{ primary }}{% endblocktrans %}</p> | ||||||
|  | 
 | ||||||
|  |         {% if available_phone_methods %} | ||||||
|  |           <h2>{% trans "Backup Phone Numbers" %}</h2> | ||||||
|  |           <p>{% blocktrans trimmed %}If your primary method is not available, we are able to | ||||||
|  |             send backup tokens to the phone numbers listed below.{% endblocktrans %}</p> | ||||||
|  |           <ul> | ||||||
|  |             {% for phone in backup_phones %} | ||||||
|  |               <li> | ||||||
|  |                 {{ phone|as_action }} | ||||||
|  |                 <form method="post" action="{% url 'two_factor:phone_delete' phone.id %}" | ||||||
|  |                       onsubmit="return confirm({% trans 'Are you sure?' %})"> | ||||||
|  |                   {% csrf_token %} | ||||||
|  |                   <button class="btn btn-sm btn-warning" | ||||||
|  |                           type="submit">{% trans "Unregister" %}</button> | ||||||
|  |                 </form> | ||||||
|  |               </li> | ||||||
|  |             {% endfor %} | ||||||
|  |           </ul> | ||||||
|  |           <p><a href="{% url 'two_factor:phone_create' %}" | ||||||
|  |             class="btn btn-info">{% trans "Add Phone Number" %}</a></p> | ||||||
|  |         {% endif %} | ||||||
|  |            | ||||||
|  |         <h2>{% trans "Backup Tokens" %}</h2> | ||||||
|  |         <p> | ||||||
|  |           {% blocktrans trimmed %}If you don't have any device with you, you can access | ||||||
|  |             your account using backup tokens.{% endblocktrans %} | ||||||
|  |           {% blocktrans trimmed count counter=backup_tokens %} | ||||||
|  |             You have only one backup token remaining. | ||||||
|  |           {% plural %} | ||||||
|  |             You have {{ counter }} backup tokens remaining. | ||||||
|  |           {% endblocktrans %} | ||||||
|  |         </p> | ||||||
|  |         <p><a href="{% url 'two_factor:backup_tokens' %}" | ||||||
|  |               class="btn btn-info">{% trans "Show Codes" %}</a></p> | ||||||
|  | 
 | ||||||
|  |         <h3>{% trans "Disable Two-Factor Authentication" %}</h3> | ||||||
|  |         <p>{% blocktrans trimmed %}However we strongly discourage you to do so, you can | ||||||
|  |           also disable two-factor authentication for your account.{% endblocktrans %}</p> | ||||||
|  |         <p><a class="btn btn-secondary" href="{% url 'two_factor:disable' %}"> | ||||||
|  |           {% trans "Disable Two-Factor Authentication" %}</a></p> | ||||||
|  |       {% else %} | ||||||
|  |         <p>{% blocktrans trimmed %}Two-factor authentication is not enabled for your | ||||||
|  |           account. Enable two-factor authentication for enhanced account | ||||||
|  |           security.{% endblocktrans %}</p> | ||||||
|  |         <p><a href="{% url 'two_factor:setup' %}" class="btn btn-primary"> | ||||||
|  |           {% trans "Enable Two-Factor Authentication" %}</a> | ||||||
|  |         </p> | ||||||
|  |       {% endif %} | ||||||
|  | {% endblock %} | ||||||
| @ -0,0 +1,7 @@ | |||||||
|  | {% load i18n %}<?xml version="1.0" encoding="UTF-8" ?> | ||||||
|  | <Response> | ||||||
|  |   <Gather timeout="15" numDigits="1" finishOnKey=""> | ||||||
|  |     <Say language="{{ locale }}">{% blocktrans %}Hi, this is {{ site_name }} calling. Press any key to continue.{% endblocktrans %}</Say> | ||||||
|  |   </Gather> | ||||||
|  |   <Say language="{{ locale }}">{% trans "You didn’t press any keys. Good bye." %}</Say> | ||||||
|  | </Response> | ||||||
| @ -0,0 +1,5 @@ | |||||||
|  | {% load i18n %} | ||||||
|  | {% blocktrans trimmed %} | ||||||
|  |     Your OTP token is {{ token }} | ||||||
|  | {% endblocktrans %} | ||||||
|  | 
 | ||||||
							
								
								
									
										12
									
								
								logs_collector/templates/two_factor/twilio/token.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,12 @@ | |||||||
|  | {% load i18n %}<?xml version="1.0" encoding="UTF-8" ?> | ||||||
|  | <Response> | ||||||
|  |   <Say language="{{ locale }}">{% trans "Your token is:" %}</Say> | ||||||
|  |   <Pause> | ||||||
|  | {% for digit in token %}  <Say language="{{ locale }}">{{ digit }}</Say> | ||||||
|  |   <Pause> | ||||||
|  | {% endfor %}  <Say language="{{ locale }}">{% trans "Repeat:" %}</Say> | ||||||
|  |   <Pause> | ||||||
|  | {% for digit in token %}  <Say language="{{ locale }}">{{ digit }}</Say> | ||||||
|  |   <Pause> | ||||||
|  | {% endfor %}  <Say language="{{ locale }}">{% trans "Good bye." %}</Say> | ||||||
|  | </Response> | ||||||
							
								
								
									
										416
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						| @ -17,6 +17,35 @@ typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} | |||||||
| [package.extras] | [package.extras] | ||||||
| tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] | tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "attrs" | ||||||
|  | version = "23.1.0" | ||||||
|  | description = "Classes Without Boilerplate" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, | ||||||
|  |     {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [package.extras] | ||||||
|  | cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] | ||||||
|  | dev = ["attrs[docs,tests]", "pre-commit"] | ||||||
|  | docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] | ||||||
|  | tests = ["attrs[tests-no-zope]", "zope-interface"] | ||||||
|  | tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "colorama" | ||||||
|  | version = "0.4.6" | ||||||
|  | description = "Cross-platform colored terminal text." | ||||||
|  | optional = false | ||||||
|  | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, | ||||||
|  |     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "crispy-bootstrap5" | name = "crispy-bootstrap5" | ||||||
| version = "0.7" | version = "0.7" | ||||||
| @ -96,6 +125,96 @@ develop = ["coverage[toml] (>=5.0a4)", "furo (>=2021.8.17b43,<2021.9.dev0)", "py | |||||||
| docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] | docs = ["furo (>=2021.8.17b43,<2021.9.dev0)", "sphinx (>=3.5.0)", "sphinx-notfound-page"] | ||||||
| testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] | testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)"] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "django-filter" | ||||||
|  | version = "23.2" | ||||||
|  | description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "django-filter-23.2.tar.gz", hash = "sha256:2fe15f78108475eda525692813205fa6f9e8c1caf1ae65daa5862d403c6dbf00"}, | ||||||
|  |     {file = "django_filter-23.2-py3-none-any.whl", hash = "sha256:d12d8e0fc6d3eb26641e553e5d53b191eb8cec611427d4bdce0becb1f7c172b5"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [package.dependencies] | ||||||
|  | Django = ">=3.2" | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "django-formtools" | ||||||
|  | version = "2.4.1" | ||||||
|  | description = "A set of high-level abstractions for Django forms" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "django-formtools-2.4.1.tar.gz", hash = "sha256:21f8d5dac737f1e636fa8a0a10969c1c32f525a6dfa27c29592827ba70d9643a"}, | ||||||
|  |     {file = "django_formtools-2.4.1-py3-none-any.whl", hash = "sha256:49ea8a64ddef4728a558bf5f8f622c0f4053b979edcf193bf00dd80432ab2f12"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [package.dependencies] | ||||||
|  | Django = ">=3.2" | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "django-otp" | ||||||
|  | version = "1.2.2" | ||||||
|  | description = "A pluggable framework for adding two-factor authentication to Django using one-time passwords." | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "django_otp-1.2.2-py3-none-any.whl", hash = "sha256:90765d5dac238a719f9550ac05681dd6307f513a81a10b6adb879b4abc6bc1a3"}, | ||||||
|  |     {file = "django_otp-1.2.2.tar.gz", hash = "sha256:007a6354dabb3a1a54574bf73abf045ebbde0bb8734a38e2ed7845ba450f345e"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [package.dependencies] | ||||||
|  | django = ">=3.2" | ||||||
|  | 
 | ||||||
|  | [package.extras] | ||||||
|  | qrcode = ["qrcode"] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "django-phonenumber-field" | ||||||
|  | version = "6.4.0" | ||||||
|  | description = "An international phone number field for django models." | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "django-phonenumber-field-6.4.0.tar.gz", hash = "sha256:72a3e7a3e7493bf2a12c07a3bc77ce89813acc16592bf04d0eee3b5a452097ed"}, | ||||||
|  |     {file = "django_phonenumber_field-6.4.0-py3-none-any.whl", hash = "sha256:a31b4f05ac0ff898661516c84940f83adb5cdcf0ae4b9b1d31bb8ad3ff345b58"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [package.dependencies] | ||||||
|  | Django = ">=3.2" | ||||||
|  | 
 | ||||||
|  | [package.extras] | ||||||
|  | phonenumbers = ["phonenumbers (>=7.0.2)"] | ||||||
|  | phonenumberslite = ["phonenumberslite (>=7.0.2)"] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "django-two-factor-auth" | ||||||
|  | version = "1.15.3" | ||||||
|  | description = "Complete Two-Factor Authentication for Django" | ||||||
|  | optional = false | ||||||
|  | python-versions = "*" | ||||||
|  | files = [ | ||||||
|  |     {file = "django-two-factor-auth-1.15.3.tar.gz", hash = "sha256:311b7f0c5ee47ae5c3734f7810f90c9390b3aef556f58a767b0d80d6b54013fb"}, | ||||||
|  |     {file = "django_two_factor_auth-1.15.3-py3-none-any.whl", hash = "sha256:a5752732225304ba0461ec6f5347def517bdde98685e90bb7879aa066c91f3a4"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [package.dependencies] | ||||||
|  | Django = ">=3.2" | ||||||
|  | django-formtools = "*" | ||||||
|  | django-otp = ">=0.8.0" | ||||||
|  | django-phonenumber-field = ">=1.1.0,<7" | ||||||
|  | phonenumberslite = {version = ">=7.0.9,<8.99", optional = true, markers = "extra == \"phonenumberslite\""} | ||||||
|  | qrcode = ">=4.0.0,<7.99" | ||||||
|  | 
 | ||||||
|  | [package.extras] | ||||||
|  | call = ["twilio (>=6.0)"] | ||||||
|  | phonenumbers = ["phonenumbers (>=7.0.9,<8.99)"] | ||||||
|  | phonenumberslite = ["phonenumberslite (>=7.0.9,<8.99)"] | ||||||
|  | sms = ["twilio (>=6.0)"] | ||||||
|  | webauthn = ["pydantic (>=1.9.0,<1.99)", "webauthn (>=1.6.0,<1.99)"] | ||||||
|  | yubikey = ["django-otp-yubikey"] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "djangorestframework" | name = "djangorestframework" | ||||||
| version = "3.14.0" | version = "3.14.0" | ||||||
| @ -135,6 +254,29 @@ lint = ["flake8", "isort", "pep8"] | |||||||
| python-jose = ["python-jose (==3.3.0)"] | python-jose = ["python-jose (==3.3.0)"] | ||||||
| test = ["cryptography", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "tox"] | test = ["cryptography", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "tox"] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "drf-spectacular" | ||||||
|  | version = "0.26.4" | ||||||
|  | description = "Sane and flexible OpenAPI 3 schema generation for Django REST framework" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.6" | ||||||
|  | files = [ | ||||||
|  |     {file = "drf-spectacular-0.26.4.tar.gz", hash = "sha256:8f5a8f87353d1bb8dcb3f3909b7109b2dcbe1d91f3e069409cf322963e140bd6"}, | ||||||
|  |     {file = "drf_spectacular-0.26.4-py3-none-any.whl", hash = "sha256:afeccc6533dcdb4e78afbfcc49f3c5e9c369aeb62f965e4d1a43b165449c147a"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [package.dependencies] | ||||||
|  | Django = ">=2.2" | ||||||
|  | djangorestframework = ">=3.10.3" | ||||||
|  | inflection = ">=0.3.1" | ||||||
|  | jsonschema = ">=2.6.0" | ||||||
|  | PyYAML = ">=5.1" | ||||||
|  | uritemplate = ">=2.0.0" | ||||||
|  | 
 | ||||||
|  | [package.extras] | ||||||
|  | offline = ["drf-spectacular-sidecar"] | ||||||
|  | sidecar = ["drf-spectacular-sidecar"] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "flake8" | name = "flake8" | ||||||
| version = "6.0.0" | version = "6.0.0" | ||||||
| @ -151,6 +293,52 @@ mccabe = ">=0.7.0,<0.8.0" | |||||||
| pycodestyle = ">=2.10.0,<2.11.0" | pycodestyle = ">=2.10.0,<2.11.0" | ||||||
| pyflakes = ">=3.0.0,<3.1.0" | pyflakes = ">=3.0.0,<3.1.0" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "inflection" | ||||||
|  | version = "0.5.1" | ||||||
|  | description = "A port of Ruby on Rails inflector to Python" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.5" | ||||||
|  | files = [ | ||||||
|  |     {file = "inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"}, | ||||||
|  |     {file = "inflection-0.5.1.tar.gz", hash = "sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "jsonschema" | ||||||
|  | version = "4.19.0" | ||||||
|  | description = "An implementation of JSON Schema validation for Python" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.8" | ||||||
|  | files = [ | ||||||
|  |     {file = "jsonschema-4.19.0-py3-none-any.whl", hash = "sha256:043dc26a3845ff09d20e4420d6012a9c91c9aa8999fa184e7efcfeccb41e32cb"}, | ||||||
|  |     {file = "jsonschema-4.19.0.tar.gz", hash = "sha256:6e1e7569ac13be8139b2dd2c21a55d350066ee3f80df06c608b398cdc6f30e8f"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [package.dependencies] | ||||||
|  | attrs = ">=22.2.0" | ||||||
|  | jsonschema-specifications = ">=2023.03.6" | ||||||
|  | referencing = ">=0.28.4" | ||||||
|  | rpds-py = ">=0.7.1" | ||||||
|  | 
 | ||||||
|  | [package.extras] | ||||||
|  | format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] | ||||||
|  | format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=1.11)"] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "jsonschema-specifications" | ||||||
|  | version = "2023.7.1" | ||||||
|  | description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.8" | ||||||
|  | files = [ | ||||||
|  |     {file = "jsonschema_specifications-2023.7.1-py3-none-any.whl", hash = "sha256:05adf340b659828a004220a9613be00fa3f223f2b82002e273dee62fd50524b1"}, | ||||||
|  |     {file = "jsonschema_specifications-2023.7.1.tar.gz", hash = "sha256:c91a50404e88a1f6ba40636778e2ee08f6e24c5613fe4c53ac24578a5a7f72bb"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [package.dependencies] | ||||||
|  | referencing = ">=0.28.0" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "markdown" | name = "markdown" | ||||||
| version = "3.4.4" | version = "3.4.4" | ||||||
| @ -177,6 +365,17 @@ files = [ | |||||||
|     {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, |     {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "phonenumberslite" | ||||||
|  | version = "8.13.18" | ||||||
|  | description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." | ||||||
|  | optional = false | ||||||
|  | python-versions = "*" | ||||||
|  | files = [ | ||||||
|  |     {file = "phonenumberslite-8.13.18-py2.py3-none-any.whl", hash = "sha256:40cef03b24f2bc5711fed2b53b72770ff58f6b7dbfff749822c91078d6e82481"}, | ||||||
|  |     {file = "phonenumberslite-8.13.18.tar.gz", hash = "sha256:a321f0decf3e4e080f005fda3fba5a791d9d14a3ca217974345ff452923c31e2"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "pycodestyle" | name = "pycodestyle" | ||||||
| version = "2.10.0" | version = "2.10.0" | ||||||
| @ -216,6 +415,17 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte | |||||||
| docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] | docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] | ||||||
| tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] | tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "pypng" | ||||||
|  | version = "0.20220715.0" | ||||||
|  | description = "Pure Python library for saving and loading PNG images" | ||||||
|  | optional = false | ||||||
|  | python-versions = "*" | ||||||
|  | files = [ | ||||||
|  |     {file = "pypng-0.20220715.0-py3-none-any.whl", hash = "sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c"}, | ||||||
|  |     {file = "pypng-0.20220715.0.tar.gz", hash = "sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "pytz" | name = "pytz" | ||||||
| version = "2023.3" | version = "2023.3" | ||||||
| @ -227,6 +437,199 @@ files = [ | |||||||
|     {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, |     {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"}, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "pyyaml" | ||||||
|  | version = "6.0.1" | ||||||
|  | description = "YAML parser and emitter for Python" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.6" | ||||||
|  | files = [ | ||||||
|  |     {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, | ||||||
|  |     {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, | ||||||
|  |     {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "qrcode" | ||||||
|  | version = "7.4.2" | ||||||
|  | description = "QR Code image generator" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.7" | ||||||
|  | files = [ | ||||||
|  |     {file = "qrcode-7.4.2-py3-none-any.whl", hash = "sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a"}, | ||||||
|  |     {file = "qrcode-7.4.2.tar.gz", hash = "sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [package.dependencies] | ||||||
|  | colorama = {version = "*", markers = "platform_system == \"Windows\""} | ||||||
|  | pypng = "*" | ||||||
|  | typing-extensions = "*" | ||||||
|  | 
 | ||||||
|  | [package.extras] | ||||||
|  | all = ["pillow (>=9.1.0)", "pytest", "pytest-cov", "tox", "zest.releaser[recommended]"] | ||||||
|  | dev = ["pytest", "pytest-cov", "tox"] | ||||||
|  | maintainer = ["zest.releaser[recommended]"] | ||||||
|  | pil = ["pillow (>=9.1.0)"] | ||||||
|  | test = ["coverage", "pytest"] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "referencing" | ||||||
|  | version = "0.30.2" | ||||||
|  | description = "JSON Referencing + Python" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.8" | ||||||
|  | files = [ | ||||||
|  |     {file = "referencing-0.30.2-py3-none-any.whl", hash = "sha256:449b6669b6121a9e96a7f9e410b245d471e8d48964c67113ce9afe50c8dd7bdf"}, | ||||||
|  |     {file = "referencing-0.30.2.tar.gz", hash = "sha256:794ad8003c65938edcdbc027f1933215e0d0ccc0291e3ce20a4d87432b59efc0"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [package.dependencies] | ||||||
|  | attrs = ">=22.2.0" | ||||||
|  | rpds-py = ">=0.7.0" | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "rpds-py" | ||||||
|  | version = "0.9.2" | ||||||
|  | description = "Python bindings to Rust's persistent data structures (rpds)" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.8" | ||||||
|  | files = [ | ||||||
|  |     {file = "rpds_py-0.9.2-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:ab6919a09c055c9b092798ce18c6c4adf49d24d4d9e43a92b257e3f2548231e7"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d55777a80f78dd09410bd84ff8c95ee05519f41113b2df90a69622f5540c4f8b"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a216b26e5af0a8e265d4efd65d3bcec5fba6b26909014effe20cd302fd1138fa"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29cd8bfb2d716366a035913ced99188a79b623a3512292963d84d3e06e63b496"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44659b1f326214950a8204a248ca6199535e73a694be8d3e0e869f820767f12f"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:745f5a43fdd7d6d25a53ab1a99979e7f8ea419dfefebcab0a5a1e9095490ee5e"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a987578ac5214f18b99d1f2a3851cba5b09f4a689818a106c23dbad0dfeb760f"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf4151acb541b6e895354f6ff9ac06995ad9e4175cbc6d30aaed08856558201f"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:03421628f0dc10a4119d714a17f646e2837126a25ac7a256bdf7c3943400f67f"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13b602dc3e8dff3063734f02dcf05111e887f301fdda74151a93dbbc249930fe"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fae5cb554b604b3f9e2c608241b5d8d303e410d7dfb6d397c335f983495ce7f6"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp310-none-win32.whl", hash = "sha256:47c5f58a8e0c2c920cc7783113df2fc4ff12bf3a411d985012f145e9242a2764"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp310-none-win_amd64.whl", hash = "sha256:4ea6b73c22d8182dff91155af018b11aac9ff7eca085750455c5990cb1cfae6e"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:e564d2238512c5ef5e9d79338ab77f1cbbda6c2d541ad41b2af445fb200385e3"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f411330a6376fb50e5b7a3e66894e4a39e60ca2e17dce258d53768fea06a37bd"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e7521f5af0233e89939ad626b15278c71b69dc1dfccaa7b97bd4cdf96536bb7"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8d3335c03100a073883857e91db9f2e0ef8a1cf42dc0369cbb9151c149dbbc1b"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d25b1c1096ef0447355f7293fbe9ad740f7c47ae032c2884113f8e87660d8f6e"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a5d3fbd02efd9cf6a8ffc2f17b53a33542f6b154e88dd7b42ef4a4c0700fdad"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5934e2833afeaf36bd1eadb57256239785f5af0220ed8d21c2896ec4d3a765f"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:095b460e117685867d45548fbd8598a8d9999227e9061ee7f012d9d264e6048d"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:91378d9f4151adc223d584489591dbb79f78814c0734a7c3bfa9c9e09978121c"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:24a81c177379300220e907e9b864107614b144f6c2a15ed5c3450e19cf536fae"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:de0b6eceb46141984671802d412568d22c6bacc9b230174f9e55fc72ef4f57de"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp311-none-win32.whl", hash = "sha256:700375326ed641f3d9d32060a91513ad668bcb7e2cffb18415c399acb25de2ab"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp311-none-win_amd64.whl", hash = "sha256:0766babfcf941db8607bdaf82569ec38107dbb03c7f0b72604a0b346b6eb3298"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1440c291db3f98a914e1afd9d6541e8fc60b4c3aab1a9008d03da4651e67386"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0f2996fbac8e0b77fd67102becb9229986396e051f33dbceada3debaacc7033f"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f30d205755566a25f2ae0382944fcae2f350500ae4df4e795efa9e850821d82"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:159fba751a1e6b1c69244e23ba6c28f879a8758a3e992ed056d86d74a194a0f3"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1f044792e1adcea82468a72310c66a7f08728d72a244730d14880cd1dabe36b"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9251eb8aa82e6cf88510530b29eef4fac825a2b709baf5b94a6094894f252387"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01899794b654e616c8625b194ddd1e5b51ef5b60ed61baa7a2d9c2ad7b2a4238"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0c43f8ae8f6be1d605b0465671124aa8d6a0e40f1fb81dcea28b7e3d87ca1e1"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:207f57c402d1f8712618f737356e4b6f35253b6d20a324d9a47cb9f38ee43a6b"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b52e7c5ae35b00566d244ffefba0f46bb6bec749a50412acf42b1c3f402e2c90"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:978fa96dbb005d599ec4fd9ed301b1cc45f1a8f7982d4793faf20b404b56677d"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6aa8326a4a608e1c28da191edd7c924dff445251b94653988efb059b16577a4d"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:aad51239bee6bff6823bbbdc8ad85136c6125542bbc609e035ab98ca1e32a192"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd4dc3602370679c2dfb818d9c97b1137d4dd412230cfecd3c66a1bf388a196"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd9da77c6ec1f258387957b754f0df60766ac23ed698b61941ba9acccd3284d1"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:190ca6f55042ea4649ed19c9093a9be9d63cd8a97880106747d7147f88a49d18"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:876bf9ed62323bc7dcfc261dbc5572c996ef26fe6406b0ff985cbcf460fc8a4c"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa2818759aba55df50592ecbc95ebcdc99917fa7b55cc6796235b04193eb3c55"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9ea4d00850ef1e917815e59b078ecb338f6a8efda23369677c54a5825dbebb55"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5855c85eb8b8a968a74dc7fb014c9166a05e7e7a8377fb91d78512900aadd13d"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:14c408e9d1a80dcb45c05a5149e5961aadb912fff42ca1dd9b68c0044904eb32"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:65a0583c43d9f22cb2130c7b110e695fff834fd5e832a776a107197e59a1898e"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp38-none-win32.whl", hash = "sha256:71f2f7715935a61fa3e4ae91d91b67e571aeb5cb5d10331ab681256bda2ad920"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp38-none-win_amd64.whl", hash = "sha256:674c704605092e3ebbbd13687b09c9f78c362a4bc710343efe37a91457123044"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:07e2c54bef6838fa44c48dfbc8234e8e2466d851124b551fc4e07a1cfeb37260"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7fdf55283ad38c33e35e2855565361f4bf0abd02470b8ab28d499c663bc5d7c"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:890ba852c16ace6ed9f90e8670f2c1c178d96510a21b06d2fa12d8783a905193"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50025635ba8b629a86d9d5474e650da304cb46bbb4d18690532dd79341467846"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:517cbf6e67ae3623c5127206489d69eb2bdb27239a3c3cc559350ef52a3bbf0b"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0836d71ca19071090d524739420a61580f3f894618d10b666cf3d9a1688355b1"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c439fd54b2b9053717cca3de9583be6584b384d88d045f97d409f0ca867d80f"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f68996a3b3dc9335037f82754f9cdbe3a95db42bde571d8c3be26cc6245f2324"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7d68dc8acded354c972116f59b5eb2e5864432948e098c19fe6994926d8e15c3"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f963c6b1218b96db85fc37a9f0851eaf8b9040aa46dec112611697a7023da535"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5a46859d7f947061b4010e554ccd1791467d1b1759f2dc2ec9055fa239f1bc26"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp39-none-win32.whl", hash = "sha256:e07e5dbf8a83c66783a9fe2d4566968ea8c161199680e8ad38d53e075df5f0d0"}, | ||||||
|  |     {file = "rpds_py-0.9.2-cp39-none-win_amd64.whl", hash = "sha256:682726178138ea45a0766907957b60f3a1bf3acdf212436be9733f28b6c5af3c"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:196cb208825a8b9c8fc360dc0f87993b8b260038615230242bf18ec84447c08d"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c7671d45530fcb6d5e22fd40c97e1e1e01965fc298cbda523bb640f3d923b387"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83b32f0940adec65099f3b1c215ef7f1d025d13ff947975a055989cb7fd019a4"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f67da97f5b9eac838b6980fc6da268622e91f8960e083a34533ca710bec8611"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03975db5f103997904c37e804e5f340c8fdabbb5883f26ee50a255d664eed58c"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:987b06d1cdb28f88a42e4fb8a87f094e43f3c435ed8e486533aea0bf2e53d931"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c861a7e4aef15ff91233751619ce3a3d2b9e5877e0fcd76f9ea4f6847183aa16"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02938432352359805b6da099c9c95c8a0547fe4b274ce8f1a91677401bb9a45f"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ef1f08f2a924837e112cba2953e15aacfccbbfcd773b4b9b4723f8f2ddded08e"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:35da5cc5cb37c04c4ee03128ad59b8c3941a1e5cd398d78c37f716f32a9b7f67"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:141acb9d4ccc04e704e5992d35472f78c35af047fa0cfae2923835d153f091be"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79f594919d2c1a0cc17d1988a6adaf9a2f000d2e1048f71f298b056b1018e872"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:a06418fe1155e72e16dddc68bb3780ae44cebb2912fbd8bb6ff9161de56e1798"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b2eb034c94b0b96d5eddb290b7b5198460e2d5d0c421751713953a9c4e47d10"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b08605d248b974eb02f40bdcd1a35d3924c83a2a5e8f5d0fa5af852c4d960af"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0805911caedfe2736935250be5008b261f10a729a303f676d3d5fea6900c96a"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab2299e3f92aa5417d5e16bb45bb4586171c1327568f638e8453c9f8d9e0f020"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c8d7594e38cf98d8a7df25b440f684b510cf4627fe038c297a87496d10a174f"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b9ec12ad5f0a4625db34db7e0005be2632c1013b253a4a60e8302ad4d462afd"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1fcdee18fea97238ed17ab6478c66b2095e4ae7177e35fb71fbe561a27adf620"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:933a7d5cd4b84f959aedeb84f2030f0a01d63ae6cf256629af3081cf3e3426e8"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:686ba516e02db6d6f8c279d1641f7067ebb5dc58b1d0536c4aaebb7bf01cdc5d"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:0173c0444bec0a3d7d848eaeca2d8bd32a1b43f3d3fde6617aac3731fa4be05f"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d576c3ef8c7b2d560e301eb33891d1944d965a4d7a2eacb6332eee8a71827db6"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed89861ee8c8c47d6beb742a602f912b1bb64f598b1e2f3d758948721d44d468"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1054a08e818f8e18910f1bee731583fe8f899b0a0a5044c6e680ceea34f93876"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99e7c4bb27ff1aab90dcc3e9d37ee5af0231ed98d99cb6f5250de28889a3d502"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c545d9d14d47be716495076b659db179206e3fd997769bc01e2d550eeb685596"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9039a11bca3c41be5a58282ed81ae422fa680409022b996032a43badef2a3752"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fb39aca7a64ad0c9490adfa719dbeeb87d13be137ca189d2564e596f8ba32c07"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2d8b3b3a2ce0eaa00c5bbbb60b6713e94e7e0becab7b3db6c5c77f979e8ed1f1"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:99b1c16f732b3a9971406fbfe18468592c5a3529585a45a35adbc1389a529a03"}, | ||||||
|  |     {file = "rpds_py-0.9.2-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c27ee01a6c3223025f4badd533bea5e87c988cb0ba2811b690395dfe16088cfe"}, | ||||||
|  |     {file = "rpds_py-0.9.2.tar.gz", hash = "sha256:8d70e8f14900f2657c249ea4def963bed86a29b81f81f5b76b5a9215680de945"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "sqlparse" | name = "sqlparse" | ||||||
| version = "0.4.4" | version = "0.4.4" | ||||||
| @ -265,7 +668,18 @@ files = [ | |||||||
|     {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, |     {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "uritemplate" | ||||||
|  | version = "4.1.1" | ||||||
|  | description = "Implementation of RFC 6570 URI Templates" | ||||||
|  | optional = false | ||||||
|  | python-versions = ">=3.6" | ||||||
|  | files = [ | ||||||
|  |     {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, | ||||||
|  |     {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [metadata] | [metadata] | ||||||
| lock-version = "2.0" | lock-version = "2.0" | ||||||
| python-versions = "^3.10" | python-versions = "^3.10" | ||||||
| content-hash = "1fc8f57d5dd1b53de4879607d1c7e7c8d9acafd9cbafe4ff64763766d69a4d51" | content-hash = "69aa50d072f03697f4d11a333694d01f2528e433185216e3dc8d32b2debe7432" | ||||||
|  | |||||||
| @ -16,6 +16,9 @@ django-cleanup = "^8.0.0" | |||||||
| django-crispy-forms = "^2.0" | django-crispy-forms = "^2.0" | ||||||
| crispy-bootstrap5 = "^0.7" | crispy-bootstrap5 = "^0.7" | ||||||
| markdown = "^3.4.4" | markdown = "^3.4.4" | ||||||
|  | django-filter = "^23.2" | ||||||
|  | drf-spectacular = "^0.26.4" | ||||||
|  | django-two-factor-auth = {extras = ["phonenumberslite"], version = "^1.15.3"} | ||||||
| 
 | 
 | ||||||
| [tool.poetry.group.dev.dependencies] | [tool.poetry.group.dev.dependencies] | ||||||
| flake8 = "^6.0.0" | flake8 = "^6.0.0" | ||||||
|  | |||||||