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):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
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
|
||||
from django.conf import settings
|
||||
import django.core.files.storage
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import pathlib
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -21,7 +23,7 @@ class Migration(migrations.Migration):
|
||||
name='Platform',
|
||||
fields=[
|
||||
('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)),
|
||||
],
|
||||
),
|
||||
@ -29,26 +31,29 @@ class Migration(migrations.Migration):
|
||||
name='Ticket',
|
||||
fields=[
|
||||
('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)),
|
||||
('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_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)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-time_create'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Archive',
|
||||
fields=[
|
||||
('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)),
|
||||
('size', models.CharField(blank=True, max_length=50)),
|
||||
('sha1', models.CharField(editable=False, max_length=1024)),
|
||||
('md5', models.CharField(editable=False, max_length=1024)),
|
||||
('time_create', models.DateTimeField(auto_now_add=True)),
|
||||
('time_update', models.DateTimeField(auto_now=True)),
|
||||
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='collector.ticket')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('ticket', models.ForeignKey(db_column='ticket_number', on_delete=django.db.models.deletion.CASCADE, to='collector.ticket', to_field='number')),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
@ -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):
|
||||
# calculate sha 1 hash sum and write md5 field to db
|
||||
# calculate md5 hash sum and write md5 field to db
|
||||
with self.file.open('rb') as f:
|
||||
md5 = hashlib.md5()
|
||||
for byte_block in iter(lambda: f.read(4096), b""):
|
||||
@ -72,7 +72,7 @@ class Ticket(models.Model):
|
||||
number = models.IntegerField(unique=True, db_index=True)
|
||||
resolved = models.BooleanField(default=False)
|
||||
note = models.TextField(blank=True)
|
||||
token = models.UUIDField(default=uuid.uuid4, editable=False)
|
||||
token = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
|
||||
attempts = models.IntegerField(default=5, validators=[
|
||||
MaxValueValidator(10),
|
||||
MinValueValidator(0)
|
||||
|
@ -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 delUrl = $(this).attr("href");
|
||||
$.ajax({
|
||||
type: "delete",
|
||||
type: "DELETE",
|
||||
url: delUrl,
|
||||
headers: {
|
||||
"X-CSRFToken":CSRF,
|
||||
@ -21,12 +21,12 @@ $(function () {
|
||||
// beforeSend: function(xhr) {
|
||||
// xhr.setRequestHeader("X-CSRFToken", csrf);
|
||||
// },
|
||||
success: function (response) {
|
||||
console.log(response.status);
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
console.log(jqXHR.status);
|
||||
$(archiveListElement).hide(1500);
|
||||
},
|
||||
error: function (response) {
|
||||
console.log(response.status);
|
||||
error: function (data, textStatus, jqXHR) {
|
||||
console.log(jqXHR.status);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -37,16 +37,13 @@ $(function () {
|
||||
let resolved = false;
|
||||
let ticketStateUrl = $(this).attr("ticket-state-url")
|
||||
if ($(this).attr("ticket-state-switch") === "1") {
|
||||
console.log('Find it!!!')
|
||||
resolved = true;
|
||||
$(this).attr("ticket-state-switch", "0"); // disable
|
||||
} else {
|
||||
resolved = false;
|
||||
resolved = true;
|
||||
$(this).attr("ticket-state-switch", "1"); // enable
|
||||
}
|
||||
console.log(resolved)
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
type: "PATCH",
|
||||
url: ticketStateUrl,
|
||||
headers: {
|
||||
"X-CSRFToken":CSRF,
|
||||
@ -57,11 +54,12 @@ $(function () {
|
||||
data: JSON.stringify({
|
||||
resolved: resolved,
|
||||
}),
|
||||
success: function (response) {
|
||||
console.log(response.resolved)
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
console.log(jqXHR.status)
|
||||
},
|
||||
error: function (response) {
|
||||
console.log(response.resolved)
|
||||
error: function (data, textStatus, jqXHR) {
|
||||
console.log(data)
|
||||
console.log(jqXHR.status)
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -80,16 +78,16 @@ $(function () {
|
||||
'X-CSRFToken':CSRF,
|
||||
'Content-Type':'application/json'
|
||||
},
|
||||
success: function (response) {
|
||||
console.log(response.status);
|
||||
success: function (data, textStatus, jqXHR) {
|
||||
console.log(jqXHR.status);
|
||||
if (delDiv.length) {
|
||||
delDiv.hide(1500);
|
||||
} else {
|
||||
window.location.href = redirectUrl;
|
||||
}
|
||||
},
|
||||
error: function (response) {
|
||||
console.log(response.status);
|
||||
error: function (data, textStatus, jqXHR) {
|
||||
console.log(jqXHR.status);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,61 +1,30 @@
|
||||
{% extends 'base.html' %}
|
||||
{% 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 '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>
|
||||
<section>
|
||||
{% include 'collector/includes/navigation.html' %}
|
||||
</section>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
{% block main %}{% endblock main %}
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
<section>
|
||||
{% block footer %}{% endblock footer %}
|
||||
</section>
|
||||
</footer>
|
||||
<script src="{% static 'collector/js/bootstrap.bundle.min.js' %}"></script>
|
||||
<script src="{% static 'collector/js/bs.theme.mode.js' %}"></script>
|
||||
<script src="{% static 'collector/js/jquery-3.7.0.min.js' %}"></script>
|
||||
{% block bs %}{% endblock bs %}
|
||||
{% block jquery %}{% endblock jquery %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
{% block collector_head %}
|
||||
<title>{% block title %}{% endblock title %}</title>
|
||||
{% endblock collector_head %}
|
||||
|
||||
{% block collector_content %}
|
||||
<header>
|
||||
<section>
|
||||
{% include 'includes/navigation.html' %}
|
||||
</section>
|
||||
</header>
|
||||
<main>
|
||||
<section>
|
||||
{% block main %}{% endblock main %}
|
||||
</section>
|
||||
</main>
|
||||
<footer>
|
||||
<section>
|
||||
{% block footer %}{% endblock footer %}
|
||||
</section>
|
||||
</footer>
|
||||
{% endblock collector_content %}
|
||||
|
||||
{% block collector_scripts %}
|
||||
<script src="{% static 'collector/js/jquery-3.7.0.min.js' %}"></script>
|
||||
{% block bs %}{% endblock bs %}
|
||||
{% block jquery %}{% endblock jquery %}
|
||||
{% endblock collector_scripts %}
|
||||
|
@ -30,7 +30,7 @@
|
||||
>Cancel
|
||||
</button>
|
||||
<a
|
||||
href="{% url 'collector:download' archive.file %}"
|
||||
href="{% url 'collector_api:archive-detail' archive.id %}"
|
||||
type="button"
|
||||
class="btn btn-danger btn-archive-eraser"
|
||||
data-bs-dismiss="modal"
|
||||
|
@ -27,7 +27,7 @@
|
||||
</button>
|
||||
<a
|
||||
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"
|
||||
data-bs-dismiss="modal"
|
||||
data-jq-ticket-del-target="#div-ticket-{{ ticket.number }}"
|
||||
|
@ -9,7 +9,7 @@
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
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 %} checked {% endif %}>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends 'collector/base.html' %}
|
||||
{% load static %}
|
||||
{% load collector_extras %}
|
||||
{% block title %}<title>{{ title }}</title>{% endblock title %}
|
||||
{% block title %} {{ title }} {% endblock title %}
|
||||
{% block main %}
|
||||
<div class="container mt-3">
|
||||
<div class="row">
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends 'collector/base.html' %}
|
||||
{% load static %}
|
||||
{% load crispy_forms_tags %}
|
||||
{% block title %}<title>{{ title }}</title>{% endblock title %}
|
||||
{% block title %} {{ title }} {% endblock title %}
|
||||
{% block main %}
|
||||
<div class="container mt-3">
|
||||
<div class="card">
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% extends 'collector/base.html' %}
|
||||
{% load static %}
|
||||
{% load collector_extras %}
|
||||
{% block title %}<title>{{ title }}</title>{% endblock title %}
|
||||
{% block title %} {{ title }} {% endblock title %}
|
||||
{% block main %}
|
||||
<div class="container mt-3">
|
||||
{% csrf_token %}
|
||||
|
@ -1,20 +1,14 @@
|
||||
from django.urls import path, include
|
||||
|
||||
from rest_framework import routers
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'collector'
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'archives', views.PublicArchiveUploadViewSet)
|
||||
# █░█░█ █▀▀ █▄▄
|
||||
# ▀▄▀▄▀ ██▄ █▄█
|
||||
# -- -- -- -- --
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# █░█░█ █▀▀ █▄▄
|
||||
# ▀▄▀▄▀ ██▄ █▄█
|
||||
# -- -- -- -- --
|
||||
|
||||
# CREATE:
|
||||
path(
|
||||
'tickets/create/',
|
||||
@ -53,28 +47,4 @@ urlpatterns = [
|
||||
views.UpdateTicket.as_view(),
|
||||
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):
|
||||
# file will be uploaded to
|
||||
# MEDIA_ROOT_FOR_SENSITIVE_FILES/<ticket>/<filename>
|
||||
return f'{instance.ticket}/{filename}'
|
||||
"""
|
||||
file will be uploaded to
|
||||
MEDIA_ROOT_FOR_SENSITIVE_FILES/<ticket-token>/<filename>
|
||||
"""
|
||||
return f'{instance.ticket.number}/{filename}'
|
||||
|
||||
|
||||
# deprecated
|
||||
def get_file_size(file_path, unit='bytes'):
|
||||
file_size = os.path.getsize(file_path)
|
||||
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)
|
||||
|
||||
|
||||
# deprecated
|
||||
def is_ajax(request):
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return True
|
||||
|
@ -1,27 +1,21 @@
|
||||
import json
|
||||
from django.core.exceptions import ValidationError
|
||||
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.generic.detail import SingleObjectMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.db.models import Q
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.parsers import FormParser, MultiPartParser
|
||||
from two_factor.views import OTPRequiredMixin
|
||||
|
||||
from rest_framework import mixins
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from .models import Archive, Ticket
|
||||
from .forms import TicketForm
|
||||
from .utils import PageTitleViewMixin, is_ajax
|
||||
|
||||
from .serializers import PublicArchiveUploadSerializer
|
||||
from .models import Archive, Ticket
|
||||
from .utils import PageTitleViewMixin
|
||||
|
||||
|
||||
class ArchiveHandlerView(LoginRequiredMixin, SingleObjectMixin, generic.View):
|
||||
class ArchiveHandlerView(
|
||||
OTPRequiredMixin,
|
||||
LoginRequiredMixin,
|
||||
SingleObjectMixin,
|
||||
generic.View):
|
||||
model = Archive
|
||||
slug_field = 'file'
|
||||
slug_url_kwarg = 'path'
|
||||
@ -30,12 +24,6 @@ class ArchiveHandlerView(LoginRequiredMixin, SingleObjectMixin, generic.View):
|
||||
self.object = self.get_object()
|
||||
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):
|
||||
model = Ticket
|
||||
@ -61,12 +49,11 @@ class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView):
|
||||
return f'{self.title} - {self.kwargs.get("ticket", "update")}'
|
||||
|
||||
def form_valid(self, form):
|
||||
print(self.request.user)
|
||||
form.instance.user = self.request.user
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class ListAllTickets(PageTitleViewMixin, generic.ListView):
|
||||
class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView):
|
||||
model = Ticket
|
||||
template_name = 'collector/tickets.html'
|
||||
context_object_name = 'tickets'
|
||||
@ -85,13 +72,17 @@ class ListAllTickets(PageTitleViewMixin, generic.ListView):
|
||||
queryset = self.model.objects.filter(
|
||||
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 super().get_queryset()
|
||||
|
||||
|
||||
class ListPlatformTickets(PageTitleViewMixin, generic.ListView):
|
||||
class ListPlatformTickets(
|
||||
LoginRequiredMixin,
|
||||
PageTitleViewMixin,
|
||||
generic.ListView
|
||||
):
|
||||
model = Ticket
|
||||
template_name = 'collector/tickets.html'
|
||||
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
|
||||
template_name = 'collector/ticket.html'
|
||||
context_object_name = 'ticket'
|
||||
@ -116,109 +107,3 @@ class DetailTicket(PageTitleViewMixin, generic.DetailView):
|
||||
|
||||
def get_title(self, **kwargs):
|
||||
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
|
||||
from pathlib import Path
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
env = environ.Env(
|
||||
# set casting default value
|
||||
DEBUG=(bool, False)
|
||||
DEBUG=(bool, False),
|
||||
SECRET_KEY=(str, 'j9QGbvM9Z4otb47')
|
||||
)
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
@ -34,9 +36,18 @@ INSTALLED_APPS = [
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'collector.apps.CollectorConfig', # main app
|
||||
'account.apps.AccountConfig', # account app
|
||||
'rest_framework',
|
||||
'rest_framework_simplejwt',
|
||||
'django_filters',
|
||||
'drf_spectacular',
|
||||
"crispy_forms",
|
||||
"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
|
||||
]
|
||||
|
||||
@ -46,6 +57,7 @@ MIDDLEWARE = [
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django_otp.middleware.OTPMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
@ -55,7 +67,7 @@ ROOT_URLCONF = 'logs_collector.urls'
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'DIRS': [BASE_DIR / 'templates'],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
@ -106,7 +118,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'Asia/Irkutsk'
|
||||
TIME_ZONE = env('TZ', default='UTC')
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
@ -117,6 +129,9 @@ USE_TZ = True
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATICFILES_DIRS = [
|
||||
BASE_DIR / "static",
|
||||
]
|
||||
|
||||
# Default primary key field type
|
||||
# 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_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.contrib import admin
|
||||
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 account.utils import AdminSiteOTPRequiredMixinRedirectSetup
|
||||
|
||||
|
||||
# ? 2FA patch (Admin site protection)
|
||||
admin.site.__class__ = AdminSiteOTPRequiredMixinRedirectSetup
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('', include('collector.urls', namespace='collector')),
|
||||
path(
|
||||
'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:
|
||||
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 %}
|
||||
{% 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">
|
||||
<div class="container">
|
||||
<a
|
||||
@ -105,12 +90,29 @@
|
||||
{% if request.user.is_staff %}
|
||||
<li>
|
||||
<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>
|
||||
{% 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><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>
|
||||
</div>
|
||||
</li>
|
||||
@ -119,7 +121,7 @@
|
||||
<div class="btn-group">
|
||||
<a
|
||||
type="button"
|
||||
href="{% url 'admin:index' %}"
|
||||
href="{% url 'two_factor:login' %}"
|
||||
class="btn btn-outline-secondary"
|
||||
>
|
||||
<i class="bi bi-box-arrow-in-right"></i></i> Login
|
||||
@ -135,42 +137,7 @@
|
||||
<!-- Theme switcher-->
|
||||
<li class="nav-item dropdown">
|
||||
<div class="dropdown bd-mode-toggle">
|
||||
<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>
|
||||
{% include 'includes/theme_switcher.html' %}
|
||||
</div>
|
||||
</li>
|
||||
</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]
|
||||
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]]
|
||||
name = "crispy-bootstrap5"
|
||||
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"]
|
||||
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]]
|
||||
name = "djangorestframework"
|
||||
version = "3.14.0"
|
||||
@ -135,6 +254,29 @@ lint = ["flake8", "isort", "pep8"]
|
||||
python-jose = ["python-jose (==3.3.0)"]
|
||||
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]]
|
||||
name = "flake8"
|
||||
version = "6.0.0"
|
||||
@ -151,6 +293,52 @@ mccabe = ">=0.7.0,<0.8.0"
|
||||
pycodestyle = ">=2.10.0,<2.11.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]]
|
||||
name = "markdown"
|
||||
version = "3.4.4"
|
||||
@ -177,6 +365,17 @@ files = [
|
||||
{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]]
|
||||
name = "pycodestyle"
|
||||
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"]
|
||||
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]]
|
||||
name = "pytz"
|
||||
version = "2023.3"
|
||||
@ -227,6 +437,199 @@ files = [
|
||||
{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]]
|
||||
name = "sqlparse"
|
||||
version = "0.4.4"
|
||||
@ -265,7 +668,18 @@ files = [
|
||||
{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]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "1fc8f57d5dd1b53de4879607d1c7e7c8d9acafd9cbafe4ff64763766d69a4d51"
|
||||
content-hash = "69aa50d072f03697f4d11a333694d01f2528e433185216e3dc8d32b2debe7432"
|
||||
|
@ -16,6 +16,9 @@ django-cleanup = "^8.0.0"
|
||||
django-crispy-forms = "^2.0"
|
||||
crispy-bootstrap5 = "^0.7"
|
||||
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]
|
||||
flake8 = "^6.0.0"
|
||||
|