From bd59c40ac60af84b528366e9bc3752a6c76faaaa Mon Sep 17 00:00:00 2001 From: jedi Date: Fri, 7 Jul 2023 18:45:05 +0200 Subject: [PATCH 01/12] stash --- .build.yml | 3 +- .gitignore | 3 +- README.md | 8 ++- backend/authentication/models.py | 26 ++++--- backend/authentication/tests/test_auth.py | 1 + backend/backend/urls.py | 6 +- backend/configure.py | 8 +++ backend/shared_data/base.json | 59 ++++++++++++++++ backend/shared_data/screws.json | 30 +++++++++ backend/toolshed/admin.py | 33 ++++++++- backend/toolshed/aggregators.py | 16 +++++ backend/toolshed/api/info.py | 6 +- backend/toolshed/api/inventory.py | 3 +- backend/toolshed/api/social.py | 40 +++++++++++ .../0006_event_transaction_profile_message.py | 67 +++++++++++++++++++ backend/toolshed/models.py | 66 ++++++++++++++++++ backend/toolshed/serializers.py | 19 +++++- backend/toolshed/tests/test_api.py | 3 +- backend/toolshed/tests/test_friend.py | 18 ++++- backend/toolshed/tests/test_social.py | 41 ++++++++++++ 20 files changed, 431 insertions(+), 25 deletions(-) create mode 100644 backend/shared_data/base.json create mode 100644 backend/shared_data/screws.json create mode 100644 backend/toolshed/aggregators.py create mode 100644 backend/toolshed/api/social.py create mode 100644 backend/toolshed/migrations/0006_event_transaction_profile_message.py create mode 100644 backend/toolshed/tests/test_social.py diff --git a/.build.yml b/.build.yml index 0061d73..a97f4ca 100644 --- a/.build.yml +++ b/.build.yml @@ -13,5 +13,4 @@ steps: - apk add --no-cache gcc musl-dev python3-dev - pip install --upgrade pip && pip install -r requirements.txt - python3 configure.py - - coverage run manage.py test - - coverage report + - coverage run manage.py test && coverage report diff --git a/.gitignore b/.gitignore index 2e85c98..b338100 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,5 @@ dmypy.json staticfiles/ userfiles/ -testdata.py \ No newline at end of file +backend/templates/ +backend/testdata.py \ No newline at end of file diff --git a/README.md b/README.md index 854b54b..a2c0135 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ ``` bash git clone https://github.com/gr4yj3d1/toolshed.git ``` + or + ``` bash git clone https://git.neulandlabor.de/j3d1/toolshed.git ``` @@ -20,7 +22,9 @@ pip install -r requirements.txt python configure.py python manage.py runserver 0.0.0.0:8000 --insecure ``` -to run this in properly in production, you need to configure a webserver to serve the static files and proxy the requests to the backend, then run the backend with just `python manage.py runserver` without the `--insecure` flag. + +to run this in properly in production, you need to configure a webserver to serve the static files and proxy the +requests to the backend, then run the backend with just `python manage.py runserver` without the `--insecure` flag. ### Frontend @@ -37,8 +41,6 @@ cd toolshed/docs mkdocs serve ``` - - ## CLI Client ### Requirements diff --git a/backend/authentication/models.py b/backend/authentication/models.py index 5ef3890..8b01aa0 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -22,7 +22,8 @@ class KnownIdentity(models.Model): def __str__(self): return f"{self.username}@{self.domain}" - def is_authenticated(self): + @staticmethod + def is_authenticated(): return True def friends_or_self(self): @@ -30,9 +31,9 @@ class KnownIdentity(models.Model): public_identity=self) def verify(self, message, signature): - if len(signature) != 128 or type(signature) != str: + if len(signature) != 128 or not isinstance(signature, str): raise TypeError('Signature must be 128 characters long and a string') - if type(message) != str: + if not isinstance(message, str): raise TypeError('Message must be a string') try: VerifyKey(bytes.fromhex(self.public_key)).verify(message.encode('utf-8'), bytes.fromhex(signature)) @@ -44,14 +45,17 @@ class KnownIdentity(models.Model): class ToolshedUserManager(auth.models.BaseUserManager): def create_user(self, username, email, password, **extra_fields): domain = extra_fields.pop('domain', 'localhost') - private_key_hex = extra_fields.pop('private_key', None) - if private_key_hex and type(private_key_hex) != str: - raise TypeError('Private key must be a string or no private key must be provided') - if private_key_hex and len(private_key_hex) != 64: - raise ValueError('Private key must be 64 characters long or no private key must be provided') - if private_key_hex and not all(c in '0123456789abcdef' for c in private_key_hex): - raise ValueError('Private key must be a hexadecimal string or no private key must be provided') - private_key = SigningKey(bytes.fromhex(private_key_hex)) if private_key_hex else SigningKey.generate() + private_key_hex: str | None = extra_fields.pop('private_key', None) + if private_key_hex is not None: + if not isinstance(private_key_hex, str): + raise TypeError('Private key must be a string or no private key must be provided') + if len(private_key_hex) != 64: + raise ValueError('Private key must be 64 characters long or no private key must be provided') + if not all(c in '0123456789abcdef' for c in private_key_hex): + raise ValueError('Private key must be a hexadecimal string or no private key must be provided') + private_key = SigningKey(bytes.fromhex(private_key_hex)) + else: + private_key = SigningKey.generate() public_key = SigningKey(private_key.encode()).verify_key extra_fields['private_key'] = private_key.encode(encoder=HexEncoder).decode('utf-8') try: diff --git a/backend/authentication/tests/test_auth.py b/backend/authentication/tests/test_auth.py index 0a3caf1..480be20 100644 --- a/backend/authentication/tests/test_auth.py +++ b/backend/authentication/tests/test_auth.py @@ -1,5 +1,6 @@ import json +from django.core.exceptions import ValidationError from django.test import Client, RequestFactory from nacl.encoding import HexEncoder from nacl.signing import SigningKey diff --git a/backend/backend/urls.py b/backend/backend/urls.py index ab9c3a2..0a27070 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -13,11 +13,14 @@ 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_yasg import openapi from drf_yasg.views import get_schema_view +from backend import settings + openapi_info = openapi.Info( title="Toolshed API", default_version='v1', @@ -35,9 +38,10 @@ urlpatterns = [ path('auth/', include('authentication.api')), path('admin/', include('hostadmin.api')), path('api/', include('toolshed.api.friend')), + path('api/', include('toolshed.api.social')), path('api/', include('toolshed.api.inventory')), path('api/', include('toolshed.api.info')), path('api/', include('toolshed.api.files')), path('media/', include('files.media_urls')), path('docs/', schema_view.with_ui('swagger', cache_timeout=0), name='api-docs'), -] +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/backend/configure.py b/backend/configure.py index 67b87b5..4229da2 100755 --- a/backend/configure.py +++ b/backend/configure.py @@ -74,10 +74,18 @@ def configure(): from django.core.management import call_command call_command('migrate') + # if yesno("Do you want to create initial domains?"): + # domains = input("Enter a comma-separated list of allowed hosts: ") + # from hostadmin import Domain + # + # Domain.objects. + + # TODO check if superuser exists if yesno("Do you want to create a superuser?"): from django.core.management import call_command call_command('createsuperuser') + # TODO ask for which static directory to use and save it in .env call_command('collectstatic', '--no-input') if yesno("Do you want to import all categories, properties and tags contained in this repository?", default=True): diff --git a/backend/shared_data/base.json b/backend/shared_data/base.json new file mode 100644 index 0000000..d961bb8 --- /dev/null +++ b/backend/shared_data/base.json @@ -0,0 +1,59 @@ +{ + "categories": [ + { "name": "hardware"}, + { "name": "material"}, + { "name": "tools"} + ], + "properties": [ + { "name": "angle", "unit_symbol": "°", "unit_name": "degree", "unit_name_plural": "degrees" }, + { "name": "area", "unit_symbol": "m²", "unit_name": "square meter", "unit_name_plural": "square meters" }, + { "name": "current", "unit_symbol": "A", "unit_name": "ampere", "unit_name_plural": "amperes" }, + { "name": "diameter", "unit_symbol": "m", "unit_name": "meter", "unit_name_plural": "meters" }, + { "name": "energy", "unit_symbol": "J", "unit_name": "joule", "unit_name_plural": "joules" }, + { "name": "frequency", "unit_symbol": "Hz", "unit_name": "hertz", "unit_name_plural": "hertz" }, + { "name": "height", "unit_symbol": "m", "unit_name": "meter", "unit_name_plural": "meters" }, + { "name": "length", "unit_symbol": "m", "unit_name": "meter", "unit_name_plural": "meters" }, + { "name": "memory", "unit_symbol": "B", "unit_name": "byte", "unit_name_plural": "bytes", "base2_prefix": true }, + { "name": "power", "unit_symbol": "W", "unit_name": "watt", "unit_name_plural": "watts" }, + { "name": "price", "unit_symbol": "€", "unit_name": "euro", "unit_name_plural": "euros" }, + { "name": "speed", "unit_symbol": "m/s", "unit_name": "meter per second", "unit_name_plural": "meters per second" }, + { "name": "temperature", "unit_symbol": "°C", "unit_name": "degree Celsius", "unit_name_plural": "degrees Celsius" }, + { "name": "time", "unit_symbol": "s", "unit_name": "second", "unit_name_plural": "seconds" }, + { "name": "voltage", "unit_symbol": "V", "unit_name": "volt", "unit_name_plural": "volts" }, + { "name": "volume", "unit_symbol": "l", "unit_name": "liter", "unit_name_plural": "liters" }, + { "name": "weight", "unit_symbol": "g", "unit_name": "gram", "unit_name_plural": "grams" }, + { "name": "width", "unit_symbol": "m", "unit_name": "meter", "unit_name_plural": "meters" } + ], + "tags": [ + {"name": "bolt", "category": "hardware"}, + {"name": "chisel", "category": "tools"}, + {"name": "clamp", "category": "tools"}, + {"name": "drill", "category": "tools"}, + {"name": "ear plugs", "category": "tools"}, + {"name": "extension cord", "category": "tools"}, + {"name": "flashlight", "category": "tools"}, + {"name": "gloves", "category": "tools"}, + {"name": "goggles", "category": "tools"}, + {"name": "hammer", "category": "tools"}, + {"name": "level", "category": "tools"}, + {"name": "mask", "category": "tools"}, + {"name": "nail", "category": "hardware"}, + {"name": "nut", "category": "hardware"}, + {"name": "paint brush", "category": "tools"}, + {"name": "paint roller", "category": "tools"}, + {"name": "paint tray", "category": "tools"}, + {"name": "pliers", "category": "tools"}, + {"name": "power strip", "category": "tools"}, + {"name": "sander", "category": "tools"}, + {"name": "saw", "category": "tools"}, + {"name": "screw", "category": "hardware"}, + {"name": "screwdriver", "category": "tools"}, + {"name": "soldering iron", "category": "tools"}, + {"name": "stapler", "category": "tools"}, + {"name": "tape measure", "category": "tools"}, + {"name": "tool"}, + {"name": "vise", "category": "tools"}, + {"name": "washer", "category": "hardware"}, + {"name": "wrench", "category": "tools"} + ] +} \ No newline at end of file diff --git a/backend/shared_data/screws.json b/backend/shared_data/screws.json new file mode 100644 index 0000000..20c8b93 --- /dev/null +++ b/backend/shared_data/screws.json @@ -0,0 +1,30 @@ +{ + "depends": [ "git:base" ], + "categories": [ + { "name": "screws", "parent": "hardware"} + ], + "tags": [ + {"name": "m1", "category": "screws"}, + {"name": "m2", "category": "screws"}, + {"name": "m2.5", "category": "screws"}, + {"name": "m3", "category": "screws"}, + {"name": "m4", "category": "screws"}, + {"name": "m5", "category": "screws"}, + {"name": "m6", "category": "screws"}, + {"name": "m8", "category": "screws"}, + {"name": "m10", "category": "screws"}, + {"name": "m12", "category": "screws"}, + {"name": "m16", "category": "screws"}, + {"name": "torx", "category": "screws"}, + {"name": "hex", "category": "screws"}, + {"name": "phillips", "category": "screws"}, + {"name": "pozidriv", "category": "screws"}, + {"name": "slotted", "category": "screws"}, + {"name": "socket", "category": "screws"}, + {"name": "flat", "category": "screws"}, + {"name": "pan", "category": "screws"}, + {"name": "button", "category": "screws"}, + {"name": "countersunk", "category": "screws"}, + {"name": "round", "category": "screws"} + ] +} \ No newline at end of file diff --git a/backend/toolshed/admin.py b/backend/toolshed/admin.py index 4b3174e..40aac35 100644 --- a/backend/toolshed/admin.py +++ b/backend/toolshed/admin.py @@ -1,6 +1,14 @@ from django.contrib import admin -from toolshed.models import InventoryItem, Property, Tag, ItemProperty, ItemTag +from toolshed.models import Profile, InventoryItem, Property, Tag, ItemProperty, ItemTag + + +class ProfileAdmin(admin.ModelAdmin): + list_display = ('user', 'bio', 'location') + search_fields = ('user', 'bio', 'location') + + +admin.site.register(Profile, ProfileAdmin) class InventoryItemAdmin(admin.ModelAdmin): @@ -25,3 +33,26 @@ class TagAdmin(admin.ModelAdmin): admin.site.register(Tag, TagAdmin) + +# class ItemPropertyAdmin(admin.ModelAdmin): +# list_display = ('item', 'property', 'value') +# search_fields = ('item', 'property', 'value') +# +# +# admin.site.register(ItemProperty, ItemPropertyAdmin) +# +# +# class ItemTagAdmin(admin.ModelAdmin): +# list_display = ('item', 'tag') +# search_fields = ('item', 'tag') +# +# +# admin.site.register(ItemTag, ItemTagAdmin) + + +# class LendingPeriodAdmin(admin.ModelAdmin): +# list_display = ('item', 'start_date', 'end_date') +# search_fields = ('item', 'start_date', 'end_date') +# +# +# admin.site.register(LendingPeriod, LendingPeriodAdmin) diff --git a/backend/toolshed/aggregators.py b/backend/toolshed/aggregators.py new file mode 100644 index 0000000..4b20c53 --- /dev/null +++ b/backend/toolshed/aggregators.py @@ -0,0 +1,16 @@ +from toolshed.models import Event, Message + + +def timeline_notifications(user): + """Return a list of notifications that the user is interested in.""" + for evt in Event.objects.all(): + if evt.user == user: + yield evt + for tool in user.inventory.all(): + if evt.tool == tool: + yield evt + + +def unread_messages(user): + """Return a list of unread messages.""" + return Message.objects.filter(recipient=user, read=False) diff --git a/backend/toolshed/api/info.py b/backend/toolshed/api/info.py index 3c54412..7395c03 100644 --- a/backend/toolshed/api/info.py +++ b/backend/toolshed/api/info.py @@ -61,9 +61,11 @@ def combined_info(request, format=None): # /info/ tags = [tag.name for tag in Tag.objects.all()] properties = PropertySerializer(Property.objects.all(), many=True).data categories = [str(category) for category in Category.objects.all()] - policies = ['private', 'friends', 'internal', 'public'] + policies = InventoryItem.AVAILABILITY_POLICY_CHOICES domains = [domain.name for domain in Domain.objects.filter(open_registration=True)] - return Response({'tags': tags, 'properties': properties, 'availability_policies': policies, 'categories': categories, 'domains': domains}) + return Response( + {'tags': tags, 'properties': properties, 'availability_policies': policies, 'categories': categories, + 'domains': domains}) urlpatterns = [ diff --git a/backend/toolshed/api/inventory.py b/backend/toolshed/api/inventory.py index c39a5c8..d500c45 100644 --- a/backend/toolshed/api/inventory.py +++ b/backend/toolshed/api/inventory.py @@ -1,12 +1,13 @@ from django.db import transaction from django.urls import path -from rest_framework import routers, viewsets +from rest_framework import routers, viewsets, serializers from rest_framework.decorators import authentication_classes, api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from authentication.models import ToolshedUser, KnownIdentity from authentication.signature_auth import SignatureAuthentication +from files.models import File from toolshed.models import InventoryItem, StorageLocation from toolshed.serializers import InventoryItemSerializer, StorageLocationSerializer diff --git a/backend/toolshed/api/social.py b/backend/toolshed/api/social.py new file mode 100644 index 0000000..74bd899 --- /dev/null +++ b/backend/toolshed/api/social.py @@ -0,0 +1,40 @@ +from django.urls import path +from rest_framework.decorators import api_view, authentication_classes, permission_classes +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from authentication.signature_auth import SignatureAuthenticationLocal +from toolshed.models import Message, Profile +from toolshed.serializers import MessageSerializer, ProfileSerializer +from toolshed.aggregators import unread_messages, timeline_notifications + + +@api_view(['GET']) +@authentication_classes([SignatureAuthenticationLocal]) +@permission_classes([IsAuthenticated]) +def get_messages(request): + messages = Message.objects.filter(recipient=request.user) + return Response(MessageSerializer(messages, many=True).data) + + +@api_view(['GET']) +@authentication_classes([SignatureAuthenticationLocal]) +@permission_classes([IsAuthenticated]) +def get_profile(request): + profile = Profile.objects.get(user=request.user) + return Response(ProfileSerializer(profile).data) + + +@api_view(['GET']) +@authentication_classes([SignatureAuthenticationLocal]) +@permission_classes([IsAuthenticated]) +def get_notifications(request): + notifications = timeline_notifications(request.user) + return Response(notifications) + + +urlpatterns = [ + path('messages/', get_messages), + path('profile/', get_profile), + path('notifications/', get_notifications), +] diff --git a/backend/toolshed/migrations/0006_event_transaction_profile_message.py b/backend/toolshed/migrations/0006_event_transaction_profile_message.py new file mode 100644 index 0000000..d0b108b --- /dev/null +++ b/backend/toolshed/migrations/0006_event_transaction_profile_message.py @@ -0,0 +1,67 @@ +# Generated by Django 4.2.2 on 2024-02-23 15:30 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('files', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('toolshed', '0005_alter_inventoryitem_availability_policy'), + ] + + operations = [ + migrations.CreateModel( + name='Event', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('description', models.TextField()), + ('location', models.CharField(max_length=255)), + ('date', models.DateField()), + ('time', models.TimeField()), + ('host_username', models.CharField(max_length=255)), + ('host_domain', models.CharField(max_length=255)), + ], + ), + migrations.CreateModel( + name='Transaction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('pending', 'Pending'), ('accepted', 'Accepted'), ('rejected', 'Rejected')], default='pending', max_length=20)), + ('message', models.TextField(blank=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('item_offered', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='offered_transactions', to='toolshed.inventoryitem')), + ('item_requested', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requested_transactions', to='toolshed.inventoryitem')), + ('offerer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='offered_transactions', to=settings.AUTH_USER_MODEL)), + ('requester', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requested_transactions', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('bio', models.TextField(blank=True)), + ('location', models.CharField(blank=True, max_length=255)), + ('profile_picture', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='files.file')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Message', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('subject', models.CharField(max_length=255)), + ('body', models.TextField()), + ('read', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('recipient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='received_messages', to=settings.AUTH_USER_MODEL)), + ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_messages', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/backend/toolshed/models.py b/backend/toolshed/models.py index de32826..30d7aca 100644 --- a/backend/toolshed/models.py +++ b/backend/toolshed/models.py @@ -1,4 +1,6 @@ from django.db import models +from django.db.models.signals import post_save, pre_save +from django.dispatch import receiver from django.core.validators import MinValueValidator from django_softdelete.models import SoftDeleteModel from rest_framework.exceptions import ValidationError @@ -7,6 +9,52 @@ from authentication.models import ToolshedUser, KnownIdentity from files.models import File +# @receiver(pre_save) +# def pre_save_handler(sender, instance, *args, **kwargs): +# instance.full_clean() + + +class Event(models.Model): + name = models.CharField(max_length=255) + description = models.TextField() + location = models.CharField(max_length=255) + date = models.DateField() + time = models.TimeField() + # host = models.ForeignKey(User, on_delete=models.CASCADE, related_name='events') + host_username = models.CharField(max_length=255) + host_domain = models.CharField(max_length=255) + + +# def __str__(self): +# return self.name + + +class Message(models.Model): + sender = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='sent_messages') + recipient = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='received_messages') + subject = models.CharField(max_length=255) + body = models.TextField() + read = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + +class Profile(models.Model): + user = models.OneToOneField(ToolshedUser, on_delete=models.CASCADE) + profile_picture = models.ForeignKey(File, on_delete=models.SET_NULL, null=True, blank=True) + bio = models.TextField(blank=True) + location = models.CharField(max_length=255, blank=True) + + @receiver(post_save, sender=ToolshedUser) + def create_user_profile(sender, instance, created, **kwargs): + if created: + Profile.objects.create(user=instance) + + @receiver(post_save, sender=ToolshedUser) + def save_user_profile(sender, instance, **kwargs): + instance.profile.save() + + class Category(SoftDeleteModel): name = models.CharField(max_length=255, unique=True) description = models.TextField(null=True, blank=True) @@ -29,6 +77,7 @@ class Property(models.Model): unit_name = models.CharField(max_length=255, null=True, blank=True) unit_name_plural = models.CharField(max_length=255, null=True, blank=True) base2_prefix = models.BooleanField(default=False) + # sort_lexicographically = models.BooleanField(default=False) dimensions = models.IntegerField(null=False, blank=False, default=1, validators=[MinValueValidator(1)]) origin = models.CharField(max_length=255, null=False, blank=False) @@ -89,6 +138,23 @@ class ItemTag(models.Model): inventory_item = models.ForeignKey(InventoryItem, on_delete=models.CASCADE) +class Transaction(models.Model): + STATUS_CHOICES = ( + ('pending', 'Pending'), + ('accepted', 'Accepted'), + ('rejected', 'Rejected'), + ) + + item_requested = models.ForeignKey(InventoryItem, on_delete=models.CASCADE, related_name='requested_transactions') + item_offered = models.ForeignKey(InventoryItem, on_delete=models.CASCADE, related_name='offered_transactions') + requester = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='requested_transactions') + offerer = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='offered_transactions') + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending') + message = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class StorageLocation(models.Model): name = models.CharField(max_length=255) description = models.TextField(null=True, blank=True) diff --git a/backend/toolshed/serializers.py b/backend/toolshed/serializers.py index 73edeff..765bba7 100644 --- a/backend/toolshed/serializers.py +++ b/backend/toolshed/serializers.py @@ -1,9 +1,10 @@ from rest_framework import serializers + from authentication.models import KnownIdentity, ToolshedUser, FriendRequestIncoming from authentication.serializers import OwnerSerializer from files.models import File from files.serializers import FileSerializer -from toolshed.models import Category, Property, ItemProperty, InventoryItem, Tag, StorageLocation +from toolshed.models import Category, Property, ItemProperty, InventoryItem, Tag, Profile, Message, StorageLocation class FriendSerializer(serializers.ModelSerializer): @@ -28,6 +29,18 @@ class FriendRequestSerializer(serializers.ModelSerializer): return obj.befriender_username + '@' + obj.befriender_domain +class ProfileSerializer(serializers.Serializer): + class Meta: + model = Profile + fields = '__all__' + + +class MessageSerializer(serializers.Serializer): + class Meta: + model = Message + fields = '__all__' + + class PropertySerializer(serializers.ModelSerializer): category = serializers.SlugRelatedField(queryset=Category.objects.all(), slug_field='name') @@ -103,6 +116,8 @@ class InventoryItemSerializer(serializers.ModelSerializer): tags = validated_data.pop('tags', []) props = validated_data.pop('itemproperty_set', []) files = validated_data.pop('files', []) + # if 'category' in validated_data and validated_data['category'] == '': + # validated_data.pop('category') item = InventoryItem.objects.create(**validated_data) for tag in tags: item.tags.add(tag, through_defaults={}) @@ -127,6 +142,8 @@ class InventoryItemSerializer(serializers.ModelSerializer): def update(self, instance, validated_data): tags = validated_data.pop('tags', []) props = validated_data.pop('itemproperty_set', []) + # if 'category' in validated_data and validated_data['category'] == '': + # validated_data.pop('category') item = super().update(instance, validated_data) item.tags.clear() item.properties.clear() diff --git a/backend/toolshed/tests/test_api.py b/backend/toolshed/tests/test_api.py index c926dec..716b670 100644 --- a/backend/toolshed/tests/test_api.py +++ b/backend/toolshed/tests/test_api.py @@ -53,7 +53,8 @@ class CombinedApiTestCase(UserTestMixin, CategoryTestMixin, TagTestMixin, Proper def test_combined_api(self): response = client.get('/api/info/', self.f['local_user1']) self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['availability_policies'], ['private', 'friends', 'internal', 'public']) + self.assertEqual(response.json()['availability_policies'], [['sell', 'Sell'], ['rent', 'Rent'], ['lend', 'Lend'], + ['share', 'Share'], ['private', 'Private']]) self.assertEqual(response.json()['categories'], ['cat1', 'cat2', 'cat3', 'cat1/subcat1', 'cat1/subcat2', 'cat1/subcat1/subcat3']) self.assertEqual(response.json()['tags'], ['tag1', 'tag2', 'tag3']) diff --git a/backend/toolshed/tests/test_friend.py b/backend/toolshed/tests/test_friend.py index 31132a3..50751bf 100644 --- a/backend/toolshed/tests/test_friend.py +++ b/backend/toolshed/tests/test_friend.py @@ -369,13 +369,29 @@ class FriendRequestOutgoingTestCase(UserTestMixin, ToolshedTestCase): self.assertEqual(befriendee.friends.first().username, befriender.username) self.assertEqual(befriendee.friends.first().domain, befriender.domain) +# TODO: +# - test that the friend request is deleted after a certain amount of time +# - decline friend request Endpoint ('reject'?) +# - cancel friend request Endpoint ('retract'?) +# - drop friend Endpoint +# Szenarios: (all also with broken signature, wrong key, wrong secret, wrong author and without authorisation header) +# (plus: friend request exists already, already friends) +# - local1 requests local2, local2 accepts +# - local1 requests local2, local2 declines +# - local1 requests local2, local1 cancels +# - ext requests local, local accepts +# - ext requests local, local declines +# - ext requests local, ext cancels +# - local requests ext, ext accepts +# - local requests ext, ext declines +# - local requests ext, local cancels + class FriendRequestCombinedTestCase(UserTestMixin, ToolshedTestCase): def setUp(self): super().setUp() self.prepare_users() - print(self.f) def test_friend_request_combined(self): befriender = self.f['local_user1'] diff --git a/backend/toolshed/tests/test_social.py b/backend/toolshed/tests/test_social.py new file mode 100644 index 0000000..067185c --- /dev/null +++ b/backend/toolshed/tests/test_social.py @@ -0,0 +1,41 @@ +from django.test import Client + +from authentication.tests import SignatureAuthClient, UserTestMixin, ToolshedTestCase + +anonymous_client = Client() +client = SignatureAuthClient() + + +class MessageApiTestCase(UserTestMixin, ToolshedTestCase): + + def setUp(self): + super().setUp() + self.prepare_users() + + def test_get_messages(self): + response = client.get('/api/messages/', self.f['local_user1']) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), []) + + +class ProfileApiTestCase(UserTestMixin, ToolshedTestCase): + + def setUp(self): + super().setUp() + self.prepare_users() + + def test_get_profile(self): + response = client.get('/api/profile/', self.f['local_user1']) + self.assertEqual(response.status_code, 200) + + +class NotificationApiTestCase(UserTestMixin, ToolshedTestCase): + + def setUp(self): + super().setUp() + self.prepare_users() + + def test_get_notifications(self): + response = client.get('/api/notifications/', self.f['local_user1']) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), []) From f2db5d9dadf014be8e29efc8a9d71cffe8ea3b6c Mon Sep 17 00:00:00 2001 From: jedi Date: Thu, 7 Mar 2024 00:44:58 +0100 Subject: [PATCH 02/12] fix inconsistencies between policy api ant combined info api --- backend/.idea/.gitignore | 2 ++ backend/toolshed/api/info.py | 6 ++++-- backend/toolshed/tests/test_api.py | 3 ++- backend/toolshed/tests/test_friend.py | 1 - 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/backend/.idea/.gitignore b/backend/.idea/.gitignore index 13566b8..a9d7db9 100644 --- a/backend/.idea/.gitignore +++ b/backend/.idea/.gitignore @@ -6,3 +6,5 @@ # Datasource local storage ignored files /dataSources/ /dataSources.local.xml +# GitHub Copilot persisted chat sessions +/copilot/chatSessions diff --git a/backend/toolshed/api/info.py b/backend/toolshed/api/info.py index 3c54412..7395c03 100644 --- a/backend/toolshed/api/info.py +++ b/backend/toolshed/api/info.py @@ -61,9 +61,11 @@ def combined_info(request, format=None): # /info/ tags = [tag.name for tag in Tag.objects.all()] properties = PropertySerializer(Property.objects.all(), many=True).data categories = [str(category) for category in Category.objects.all()] - policies = ['private', 'friends', 'internal', 'public'] + policies = InventoryItem.AVAILABILITY_POLICY_CHOICES domains = [domain.name for domain in Domain.objects.filter(open_registration=True)] - return Response({'tags': tags, 'properties': properties, 'availability_policies': policies, 'categories': categories, 'domains': domains}) + return Response( + {'tags': tags, 'properties': properties, 'availability_policies': policies, 'categories': categories, + 'domains': domains}) urlpatterns = [ diff --git a/backend/toolshed/tests/test_api.py b/backend/toolshed/tests/test_api.py index c926dec..716b670 100644 --- a/backend/toolshed/tests/test_api.py +++ b/backend/toolshed/tests/test_api.py @@ -53,7 +53,8 @@ class CombinedApiTestCase(UserTestMixin, CategoryTestMixin, TagTestMixin, Proper def test_combined_api(self): response = client.get('/api/info/', self.f['local_user1']) self.assertEqual(response.status_code, 200) - self.assertEqual(response.json()['availability_policies'], ['private', 'friends', 'internal', 'public']) + self.assertEqual(response.json()['availability_policies'], [['sell', 'Sell'], ['rent', 'Rent'], ['lend', 'Lend'], + ['share', 'Share'], ['private', 'Private']]) self.assertEqual(response.json()['categories'], ['cat1', 'cat2', 'cat3', 'cat1/subcat1', 'cat1/subcat2', 'cat1/subcat1/subcat3']) self.assertEqual(response.json()['tags'], ['tag1', 'tag2', 'tag3']) diff --git a/backend/toolshed/tests/test_friend.py b/backend/toolshed/tests/test_friend.py index 31132a3..fba1a42 100644 --- a/backend/toolshed/tests/test_friend.py +++ b/backend/toolshed/tests/test_friend.py @@ -375,7 +375,6 @@ class FriendRequestCombinedTestCase(UserTestMixin, ToolshedTestCase): def setUp(self): super().setUp() self.prepare_users() - print(self.f) def test_friend_request_combined(self): befriender = self.f['local_user1'] From 8cf0897ec5df9b4a2b74f175fe4ffee0a7843ff5 Mon Sep 17 00:00:00 2001 From: jedi Date: Mon, 11 Mar 2024 17:37:56 +0100 Subject: [PATCH 03/12] make dataset parsing more robust --- backend/configure.py | 21 ++++- backend/hostadmin/admin.py | 10 ++- .../0003_importedidentifiersets_hash.py | 39 ++++++++++ ...04_alter_importedidentifiersets_options.py | 17 ++++ backend/hostadmin/models.py | 4 + backend/hostadmin/serializers.py | 71 +++++++++++++++-- backend/hostadmin/tests.py | 77 +++++++++++++++++-- backend/toolshed/admin.py | 18 +++-- ...ag_options_alter_category_name_and_more.py | 67 ++++++++++++++++ backend/toolshed/models.py | 32 ++++++-- backend/toolshed/tests/fixtures.py | 3 +- backend/toolshed/tests/test_api.py | 3 +- backend/toolshed/tests/test_category.py | 11 ++- 13 files changed, 337 insertions(+), 36 deletions(-) create mode 100644 backend/hostadmin/migrations/0003_importedidentifiersets_hash.py create mode 100644 backend/hostadmin/migrations/0004_alter_importedidentifiersets_options.py create mode 100644 backend/toolshed/migrations/0006_alter_tag_options_alter_category_name_and_more.py diff --git a/backend/configure.py b/backend/configure.py index 67b87b5..ee26cac 100755 --- a/backend/configure.py +++ b/backend/configure.py @@ -83,10 +83,12 @@ def configure(): if yesno("Do you want to import all categories, properties and tags contained in this repository?", default=True): from hostadmin.serializers import CategorySerializer, PropertySerializer, TagSerializer from hostadmin.models import ImportedIdentifierSets + from hashlib import sha256 if not os.path.exists('shared_data'): os.mkdir('shared_data') files = os.listdir('shared_data') idsets = {} + hashes = {} for file in files: if file.endswith('.json'): name = "git:" + file[:-5] @@ -94,6 +96,8 @@ def configure(): try: idset = json.load(f) idsets[name] = idset + f.seek(0) + hashes[name] = sha256(f.read().encode()).hexdigest() except json.decoder.JSONDecodeError: print('Error: invalid JSON in file {}'.format(file)) imported_sets = ImportedIdentifierSets.objects.all() @@ -108,9 +112,13 @@ def configure(): unmet_deps = [dep for dep in idset['depends'] if not imported_sets.filter(name=dep).exists()] if unmet_deps: if all([dep in idsets.keys() for dep in unmet_deps]): - print('Not all dependencies for {} are imported, postponing'.format(name)) - queue.append(name) - continue + if all([dep in queue for dep in unmet_deps]): + print('Not all dependencies for {} are imported, postponing'.format(name)) + queue.append(name) + continue + else: + print('Error: unresolvable dependencies for {}: {}'.format(name, unmet_deps)) + continue else: print('unknown dependencies for {}: {}'.format(name, unmet_deps)) continue @@ -131,10 +139,15 @@ def configure(): serializer = TagSerializer(data=tag) if serializer.is_valid(): serializer.save(origin=name) - imported_sets.create(name=name) + imported_sets.create(name=name, hash=hashes[name]) except IntegrityError: print('Error: integrity error while importing {}\n\tmight be cause by name conflicts with existing' ' categories, properties or tags'.format(name)) + transaction.set_rollback(True) + continue + except Exception as e: + print('Error: {}'.format(e)) + transaction.set_rollback(True) continue diff --git a/backend/hostadmin/admin.py b/backend/hostadmin/admin.py index 84e6155..4dbabc7 100644 --- a/backend/hostadmin/admin.py +++ b/backend/hostadmin/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Domain +from .models import Domain, ImportedIdentifierSets class DomainAdmin(admin.ModelAdmin): @@ -9,3 +9,11 @@ class DomainAdmin(admin.ModelAdmin): admin.site.register(Domain, DomainAdmin) + + +class ImportedIdentifierSetsAdmin(admin.ModelAdmin): + list_display = ('name', 'hash', 'created_at') + list_filter = ('name', 'hash', 'created_at') + + +admin.site.register(ImportedIdentifierSets, ImportedIdentifierSetsAdmin) diff --git a/backend/hostadmin/migrations/0003_importedidentifiersets_hash.py b/backend/hostadmin/migrations/0003_importedidentifiersets_hash.py new file mode 100644 index 0000000..d62713f --- /dev/null +++ b/backend/hostadmin/migrations/0003_importedidentifiersets_hash.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.2 on 2024-03-11 15:19 +import os + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('hostadmin', '0002_importedidentifiersets'), + ] + + def calculate_hash(apps, schema_editor): + from hostadmin.models import ImportedIdentifierSets + for identifier_set in ImportedIdentifierSets.objects.all(): + if not identifier_set.hash: + print("update", identifier_set.name) + filename = "shared_data/" + identifier_set.name.strip('git:') + ".json" + if not os.path.exists(filename): + continue + from hashlib import sha256 + with open(filename, 'r') as file: + data = file.read() + identifier_set.hash = sha256(data.encode()).hexdigest() + identifier_set.save() + + operations = [ + migrations.AddField( + model_name='importedidentifiersets', + name='hash', + field=models.CharField(blank=True, max_length=255, null=True), + + ), + migrations.RunPython(calculate_hash), + migrations.AlterField( + model_name='importedidentifiersets', + name='hash', + field=models.CharField(max_length=255, unique=True), + ), + ] diff --git a/backend/hostadmin/migrations/0004_alter_importedidentifiersets_options.py b/backend/hostadmin/migrations/0004_alter_importedidentifiersets_options.py new file mode 100644 index 0000000..0d0404d --- /dev/null +++ b/backend/hostadmin/migrations/0004_alter_importedidentifiersets_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.2 on 2024-03-14 16:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('hostadmin', '0003_importedidentifiersets_hash'), + ] + + operations = [ + migrations.AlterModelOptions( + name='importedidentifiersets', + options={'verbose_name_plural': 'imported identifier sets'}, + ), + ] diff --git a/backend/hostadmin/models.py b/backend/hostadmin/models.py index bac6ac1..da29811 100644 --- a/backend/hostadmin/models.py +++ b/backend/hostadmin/models.py @@ -12,4 +12,8 @@ class Domain(models.Model): class ImportedIdentifierSets(models.Model): name = models.CharField(max_length=255, unique=True) + hash = models.CharField(max_length=255, unique=True) created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + verbose_name_plural = 'imported identifier sets' diff --git a/backend/hostadmin/serializers.py b/backend/hostadmin/serializers.py index 72bacba..02fadb6 100644 --- a/backend/hostadmin/serializers.py +++ b/backend/hostadmin/serializers.py @@ -5,6 +5,32 @@ from hostadmin.models import Domain from toolshed.models import Category, Property, Tag +class SlugPathField(serializers.SlugRelatedField): + def to_internal_value(self, data): + path = data.split('/') if '/' in data else [data] + candidates = self.get_queryset().filter(name=path[-1]) + if len(candidates) == 1: + return candidates.first() + if len(candidates) == 0: + raise serializers.ValidationError( + "No {} with name '{}' found".format(self.queryset.model.__name__, path[-1])) + if len(candidates) > 1 and len(path) == 1: + raise serializers.ValidationError("Multiple {}s with name '{}' found, please specify the parent".format( + self.queryset.model.__name__, path[-1])) + parent = self.to_internal_value('/'.join(path[:-1])) + candidates = self.get_queryset().filter(name=path[-1], parent=parent) + if len(candidates) == 1: + return candidates.first() + if len(candidates) == 0: + raise serializers.ValidationError( + "No {} with name '{}' found".format(self.queryset.model.__name__, path[-1])) + + def to_representation(self, value): + source = getattr(value, self.field_name, None) # should this use self.source? + prefix = self.to_representation(source) + '/' if source else '' + return prefix + getattr(value, self.slug_field) + + class DomainSerializer(serializers.ModelSerializer): owner = OwnerSerializer(read_only=True) @@ -12,12 +38,21 @@ class DomainSerializer(serializers.ModelSerializer): model = Domain fields = ['name', 'owner', 'open_registration'] - def create(self, validated_data): - return super().create(validated_data) - class CategorySerializer(serializers.ModelSerializer): - parent = serializers.SlugRelatedField(slug_field='name', queryset=Category.objects.all(), required=False) + parent = SlugPathField(slug_field='name', queryset=Category.objects.all(), required=False) + + def validate(self, attrs): + if 'name' in attrs: + if '/' in attrs['name']: + raise serializers.ValidationError("Category name cannot contain '/'") + return attrs + + def create(self, validated_data): + try: + return Category.objects.create(**validated_data) + except Exception as e: + raise serializers.ValidationError(e) class Meta: model = Category @@ -27,7 +62,19 @@ class CategorySerializer(serializers.ModelSerializer): class PropertySerializer(serializers.ModelSerializer): - category = serializers.SlugRelatedField(slug_field='name', queryset=Category.objects.all(), required=False) + category = SlugPathField(slug_field='name', queryset=Category.objects.all(), required=False) + + def validate(self, attrs): + if 'name' in attrs: + if '/' in attrs['name']: + raise serializers.ValidationError("Property name cannot contain '/'") + return attrs + + def create(self, validated_data): + try: + return Property.objects.create(**validated_data) + except Exception as e: + raise serializers.ValidationError(e) class Meta: model = Property @@ -38,7 +85,19 @@ class PropertySerializer(serializers.ModelSerializer): class TagSerializer(serializers.ModelSerializer): - category = serializers.SlugRelatedField(slug_field='name', queryset=Category.objects.all(), required=False) + category = SlugPathField(slug_field='name', queryset=Category.objects.all(), required=False) + + def validate(self, attrs): + if 'name' in attrs: + if '/' in attrs['name']: + raise serializers.ValidationError("Tag name cannot contain '/'") + return attrs + + def create(self, validated_data): + try: + return Tag.objects.create(**validated_data) + except Exception as e: + raise serializers.ValidationError(e) class Meta: model = Tag diff --git a/backend/hostadmin/tests.py b/backend/hostadmin/tests.py index 513d705..0df45c2 100644 --- a/backend/hostadmin/tests.py +++ b/backend/hostadmin/tests.py @@ -100,7 +100,8 @@ class CategoryApiTestCase(UserTestMixin, CategoryTestMixin, ToolshedTestCase): response = client.get('/api/categories/', self.f['local_user1']) self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), - ["cat1", "cat2", "cat3", "cat1/subcat1", "cat1/subcat2", "cat1/subcat1/subcat3"]) + ["cat1", "cat2", "cat3", "cat1/subcat1", + "cat1/subcat2", "cat1/subcat1/subcat1", "cat1/subcat1/subcat2"]) def test_admin_get_categories_fail(self): response = client.get('/admin/categories/', self.f['local_user1']) @@ -109,7 +110,7 @@ class CategoryApiTestCase(UserTestMixin, CategoryTestMixin, ToolshedTestCase): def test_admin_get_categories(self): response = client.get('/admin/categories/', self.f['admin']) self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.json()), 6) + self.assertEqual(len(response.json()), 7) self.assertEqual(response.json()[0]['name'], 'cat1') self.assertEqual(response.json()[1]['name'], 'cat2') self.assertEqual(response.json()[2]['name'], 'cat3') @@ -117,10 +118,12 @@ class CategoryApiTestCase(UserTestMixin, CategoryTestMixin, ToolshedTestCase): self.assertEqual(response.json()[3]['parent'], 'cat1') self.assertEqual(response.json()[4]['name'], 'subcat2') self.assertEqual(response.json()[4]['parent'], 'cat1') - self.assertEqual(response.json()[5]['name'], 'subcat3') - self.assertEqual(response.json()[5]['parent'], 'subcat1') + self.assertEqual(response.json()[5]['name'], 'subcat1') + self.assertEqual(response.json()[5]['parent'], 'cat1/subcat1') + self.assertEqual(response.json()[6]['name'], 'subcat2') + self.assertEqual(response.json()[6]['parent'], 'cat1/subcat1') - def test_admin_create_category(self): + def test_admin_post_category(self): response = client.post('/admin/categories/', self.f['admin'], {'name': 'cat4'}) self.assertEqual(response.status_code, 201) self.assertEqual(response.json()['name'], 'cat4') @@ -128,6 +131,40 @@ class CategoryApiTestCase(UserTestMixin, CategoryTestMixin, ToolshedTestCase): self.assertEqual(response.json()['parent'], None) self.assertEqual(response.json()['origin'], 'api') + def test_admin_post_category_duplicate(self): + response = client.post('/admin/categories/', self.f['admin'], {'name': 'cat3'}) + self.assertEqual(response.status_code, 400) + + def test_admin_post_category_invalid(self): + response = client.post('/admin/categories/', self.f['admin'], {'name': 'cat/4'}) + self.assertEqual(response.status_code, 400) + + def test_admin_post_category_parent_not_found(self): + response = client.post('/admin/categories/', self.f['admin'], {'name': 'subcat4', 'parent': 'cat4'}) + self.assertEqual(response.status_code, 400) + + def test_admin_post_category_parent_ambiguous(self): + response = client.post('/admin/categories/', self.f['admin'], {'name': 'subcat4', 'parent': 'subcat1'}) + self.assertEqual(response.status_code, 400) + + def test_admin_post_category_parent_subcategory(self): + response = client.post('/admin/categories/', self.f['admin'], {'name': 'subcat4', 'parent': 'cat1/subcat1'}) + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json()['name'], 'subcat4') + self.assertEqual(response.json()['description'], None) + self.assertEqual(response.json()['parent'], 'cat1/subcat1') + self.assertEqual(response.json()['origin'], 'api') + + def test_admin_post_category_parent_subcategory_not_found(self): + response = client.post('/admin/categories/', self.f['admin'], {'name': 'subcat4', 'parent': 'cat2/subcat1'}) + self.assertEqual(response.status_code, 400) + + def test_admin_post_category_parent_subcategory_ambiguous(self): + from toolshed.models import Category + self.f['subcat111'] = Category.objects.create(name='subcat1', parent=self.f['subcat11'], origin='test') + response = client.post('/admin/categories/', self.f['admin'], {'name': 'subcat4', 'parent': 'subcat1/subcat1'}) + self.assertEqual(response.status_code, 400) + def test_admin_post_subcategory(self): response = client.post('/admin/categories/', self.f['admin'], {'name': 'subcat4', 'parent': 'cat1'}) self.assertEqual(response.status_code, 201) @@ -136,6 +173,18 @@ class CategoryApiTestCase(UserTestMixin, CategoryTestMixin, ToolshedTestCase): self.assertEqual(response.json()['parent'], 'cat1') self.assertEqual(response.json()['origin'], 'api') + def test_admin_post_subcategory_duplicate(self): + response = client.post('/admin/categories/', self.f['admin'], {'name': 'subcat2', 'parent': 'cat1'}) + self.assertEqual(response.status_code, 400) + + def test_admin_post_subcategory_distinct_duplicate(self): + response = client.post('/admin/categories/', self.f['admin'], {'name': 'subcat2', 'parent': 'cat2'}) + self.assertEqual(response.status_code, 201) + self.assertEqual(response.json()['name'], 'subcat2') + self.assertEqual(response.json()['description'], None) + self.assertEqual(response.json()['parent'], 'cat2') + self.assertEqual(response.json()['origin'], 'api') + def test_admin_put_category(self): response = client.put('/admin/categories/1/', self.f['admin'], {'name': 'cat5'}) self.assertEqual(response.status_code, 200) @@ -188,6 +237,14 @@ class TagApiTestCase(UserTestMixin, CategoryTestMixin, TagTestMixin, ToolshedTes self.assertEqual(response.json()['origin'], 'api') self.assertEqual(response.json()['category'], None) + def test_admin_create_tag_duplicate(self): + response = client.post('/admin/tags/', self.f['admin'], {'name': 'tag3'}) + self.assertEqual(response.status_code, 400) + + def test_admin_create_tag_invalid(self): + response = client.post('/admin/tags/', self.f['admin'], {'name': 'tag/4'}) + self.assertEqual(response.status_code, 400) + def test_admin_put_tag(self): response = client.put('/admin/tags/1/', self.f['admin'], {'name': 'tag5'}) self.assertEqual(response.status_code, 200) @@ -250,7 +307,13 @@ class PropertyApiTestCase(UserTestMixin, CategoryTestMixin, PropertyTestMixin, T self.assertEqual(response.json()['base2_prefix'], False) self.assertEqual(response.json()['dimensions'], 1) - # self.assertEqual(response.json()['sort_lexicographically'], False) + def test_admin_create_property_duplicate(self): + response = client.post('/admin/properties/', self.f['admin'], {'name': 'prop3', 'category': 'cat1'}) + self.assertEqual(response.status_code, 400) + + def test_admin_create_property_invalid(self): + response = client.post('/admin/properties/', self.f['admin'], {'name': 'prop/4'}) + self.assertEqual(response.status_code, 400) def test_admin_put_property(self): response = client.put('/admin/properties/1/', self.f['admin'], {'name': 'prop5'}) @@ -265,8 +328,6 @@ class PropertyApiTestCase(UserTestMixin, CategoryTestMixin, PropertyTestMixin, T self.assertEqual(response.json()['base2_prefix'], False) self.assertEqual(response.json()['dimensions'], 1) - # self.assertEqual(response.json()['sort_lexicographically'], False) - def test_admin_patch_property(self): response = client.patch('/admin/properties/1/', self.f['admin'], {'name': 'prop5'}) self.assertEqual(response.status_code, 200) diff --git a/backend/toolshed/admin.py b/backend/toolshed/admin.py index 4b3174e..aeefc0c 100644 --- a/backend/toolshed/admin.py +++ b/backend/toolshed/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from toolshed.models import InventoryItem, Property, Tag, ItemProperty, ItemTag +from toolshed.models import InventoryItem, Property, Tag, Category class InventoryItemAdmin(admin.ModelAdmin): @@ -12,16 +12,24 @@ admin.site.register(InventoryItem, InventoryItemAdmin) class PropertyAdmin(admin.ModelAdmin): - list_display = ('name',) - search_fields = ('name',) + list_display = ('name', 'description', 'category', 'unit_symbol', 'base2_prefix', 'dimensions', 'origin') + search_fields = ('name', 'description', 'category', 'unit_symbol', 'base2_prefix', 'dimensions', 'origin') admin.site.register(Property, PropertyAdmin) class TagAdmin(admin.ModelAdmin): - list_display = ('name',) - search_fields = ('name',) + list_display = ('name', 'description', 'category', 'origin') + search_fields = ('name', 'description', 'category', 'origin') admin.site.register(Tag, TagAdmin) + + +class CategoryAdmin(admin.ModelAdmin): + list_display = ('name', 'description', 'parent', 'origin') + search_fields = ('name', 'description', 'parent', 'origin') + + +admin.site.register(Category, CategoryAdmin) diff --git a/backend/toolshed/migrations/0006_alter_tag_options_alter_category_name_and_more.py b/backend/toolshed/migrations/0006_alter_tag_options_alter_category_name_and_more.py new file mode 100644 index 0000000..603a215 --- /dev/null +++ b/backend/toolshed/migrations/0006_alter_tag_options_alter_category_name_and_more.py @@ -0,0 +1,67 @@ +# Generated by Django 4.2.2 on 2024-03-14 16:54 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('toolshed', '0005_alter_inventoryitem_availability_policy'), + ] + + operations = [ + migrations.AlterModelOptions( + name='tag', + options={'verbose_name_plural': 'tags'}, + ), + migrations.AlterField( + model_name='category', + name='name', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='category', + name='parent', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='toolshed.category'), + ), + migrations.AlterField( + model_name='inventoryitem', + name='category', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inventory_items', to='toolshed.category'), + ), + migrations.AlterField( + model_name='property', + name='category', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='properties', to='toolshed.category'), + ), + migrations.AlterField( + model_name='tag', + name='category', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tags', to='toolshed.category'), + ), + migrations.AddConstraint( + model_name='category', + constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', False)), fields=('name', 'parent'), name='category_unique_name_parent'), + ), + migrations.AddConstraint( + model_name='category', + constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('name',), name='category_unique_name_no_parent'), + ), + migrations.AddConstraint( + model_name='property', + constraint=models.UniqueConstraint(condition=models.Q(('category__isnull', False)), fields=('name', 'category'), name='property_unique_name_category'), + ), + migrations.AddConstraint( + model_name='property', + constraint=models.UniqueConstraint(condition=models.Q(('category__isnull', True)), fields=('name',), name='property_unique_name_no_category'), + ), + migrations.AddConstraint( + model_name='tag', + constraint=models.UniqueConstraint(condition=models.Q(('category__isnull', False)), fields=('name', 'category'), name='tag_unique_name_category'), + ), + migrations.AddConstraint( + model_name='tag', + constraint=models.UniqueConstraint(condition=models.Q(('category__isnull', True)), fields=('name',), name='tag_unique_name_no_category'), + ), + ] diff --git a/backend/toolshed/models.py b/backend/toolshed/models.py index de32826..2dd0713 100644 --- a/backend/toolshed/models.py +++ b/backend/toolshed/models.py @@ -8,13 +8,19 @@ from files.models import File class Category(SoftDeleteModel): - name = models.CharField(max_length=255, unique=True) + name = models.CharField(max_length=255) description = models.TextField(null=True, blank=True) - parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children') + parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, related_name='children') origin = models.CharField(max_length=255, null=False, blank=False) class Meta: verbose_name_plural = 'categories' + constraints = [ + models.UniqueConstraint(fields=['name', 'parent'], condition=models.Q(parent__isnull=False), + name='category_unique_name_parent'), + models.UniqueConstraint(fields=['name'], condition=models.Q(parent__isnull=True), + name='category_unique_name_no_parent') + ] def __str__(self): parent = str(self.parent) + "/" if self.parent else "" @@ -24,7 +30,7 @@ class Category(SoftDeleteModel): class Property(models.Model): name = models.CharField(max_length=255) description = models.TextField(null=True, blank=True) - category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True, related_name='properties') + category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, related_name='properties') unit_symbol = models.CharField(max_length=16, null=True, blank=True) unit_name = models.CharField(max_length=255, null=True, blank=True) unit_name_plural = models.CharField(max_length=255, null=True, blank=True) @@ -34,6 +40,12 @@ class Property(models.Model): class Meta: verbose_name_plural = 'properties' + constraints = [ + models.UniqueConstraint(fields=['name', 'category'], condition=models.Q(category__isnull=False), + name='property_unique_name_category'), + models.UniqueConstraint(fields=['name'], condition=models.Q(category__isnull=True), + name='property_unique_name_no_category') + ] def __str__(self): return self.name @@ -42,9 +54,18 @@ class Property(models.Model): class Tag(models.Model): name = models.CharField(max_length=255) description = models.TextField(null=True, blank=True) - category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True, related_name='tags') + category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, related_name='tags') origin = models.CharField(max_length=255, null=False, blank=False) + class Meta: + verbose_name_plural = 'tags' + constraints = [ + models.UniqueConstraint(fields=['name', 'category'], condition=models.Q(category__isnull=False), + name='tag_unique_name_category'), + models.UniqueConstraint(fields=['name'], condition=models.Q(category__isnull=True), + name='tag_unique_name_no_category') + ] + def __str__(self): return self.name @@ -61,8 +82,7 @@ class InventoryItem(SoftDeleteModel): published = models.BooleanField(default=False) name = models.CharField(max_length=255, null=True, blank=True) description = models.TextField(null=True, blank=True) - category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=True, - related_name='inventory_items') + category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, related_name='inventory_items') availability_policy = models.CharField(max_length=20, choices=AVAILABILITY_POLICY_CHOICES, default='private') owned_quantity = models.IntegerField(default=1, validators=[MinValueValidator(0)]) owner = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='inventory_items') diff --git a/backend/toolshed/tests/fixtures.py b/backend/toolshed/tests/fixtures.py index 6b866cc..5ee9d8b 100644 --- a/backend/toolshed/tests/fixtures.py +++ b/backend/toolshed/tests/fixtures.py @@ -8,7 +8,8 @@ class CategoryTestMixin: self.f['cat3'] = Category.objects.create(name='cat3', origin='test') self.f['subcat1'] = Category.objects.create(name='subcat1', parent=self.f['cat1'], origin='test') self.f['subcat2'] = Category.objects.create(name='subcat2', parent=self.f['cat1'], origin='test') - self.f['subcat3'] = Category.objects.create(name='subcat3', parent=self.f['subcat1'], origin='test') + self.f['subcat11'] = Category.objects.create(name='subcat1', parent=self.f['subcat1'], origin='test') + self.f['subcat12'] = Category.objects.create(name='subcat2', parent=self.f['subcat1'], origin='test') class TagTestMixin: diff --git a/backend/toolshed/tests/test_api.py b/backend/toolshed/tests/test_api.py index 716b670..d77a0eb 100644 --- a/backend/toolshed/tests/test_api.py +++ b/backend/toolshed/tests/test_api.py @@ -56,7 +56,8 @@ class CombinedApiTestCase(UserTestMixin, CategoryTestMixin, TagTestMixin, Proper self.assertEqual(response.json()['availability_policies'], [['sell', 'Sell'], ['rent', 'Rent'], ['lend', 'Lend'], ['share', 'Share'], ['private', 'Private']]) self.assertEqual(response.json()['categories'], - ['cat1', 'cat2', 'cat3', 'cat1/subcat1', 'cat1/subcat2', 'cat1/subcat1/subcat3']) + ['cat1', 'cat2', 'cat3', 'cat1/subcat1', 'cat1/subcat2', 'cat1/subcat1/subcat1', + 'cat1/subcat1/subcat2']) self.assertEqual(response.json()['tags'], ['tag1', 'tag2', 'tag3']) self.assertEqual([p['name'] for p in response.json()['properties']], ['prop1', 'prop2', 'prop3']) self.assertEqual(response.json()['domains'], ['example.com']) diff --git a/backend/toolshed/tests/test_category.py b/backend/toolshed/tests/test_category.py index 114e7ad..10a552c 100644 --- a/backend/toolshed/tests/test_category.py +++ b/backend/toolshed/tests/test_category.py @@ -17,10 +17,11 @@ class CategoryTestCase(CategoryTestMixin, UserTestMixin, ToolshedTestCase): self.assertEqual(self.f['cat1'].children.last(), self.f['subcat2']) self.assertEqual(self.f['subcat1'].parent, self.f['cat1']) self.assertEqual(self.f['subcat2'].parent, self.f['cat1']) - self.assertEqual(self.f['subcat1'].children.count(), 1) + self.assertEqual(self.f['subcat1'].children.count(), 2) self.assertEqual(str(self.f['subcat1']), 'cat1/subcat1') self.assertEqual(str(self.f['subcat2']), 'cat1/subcat2') - self.assertEqual(str(self.f['subcat3']), 'cat1/subcat1/subcat3') + self.assertEqual(str(self.f['subcat11']), 'cat1/subcat1/subcat1') + self.assertEqual(str(self.f['subcat12']), 'cat1/subcat1/subcat2') class CategoryApiTestCase(CategoryTestMixin, UserTestMixin, ToolshedTestCase): @@ -33,10 +34,12 @@ class CategoryApiTestCase(CategoryTestMixin, UserTestMixin, ToolshedTestCase): def test_get_categories(self): reply = client.get('/api/categories/', self.f['local_user1']) self.assertEqual(reply.status_code, 200) - self.assertEqual(len(reply.json()), 6) + self.assertEqual(len(reply.json()), 7) self.assertEqual(reply.json()[0], 'cat1') self.assertEqual(reply.json()[1], 'cat2') self.assertEqual(reply.json()[2], 'cat3') self.assertEqual(reply.json()[3], 'cat1/subcat1') self.assertEqual(reply.json()[4], 'cat1/subcat2') - self.assertEqual(reply.json()[5], 'cat1/subcat1/subcat3') + self.assertEqual(reply.json()[5], 'cat1/subcat1/subcat1') + self.assertEqual(reply.json()[6], 'cat1/subcat1/subcat2') + From bea56f101a79bf27fa68567afff9fb614b255c4f Mon Sep 17 00:00:00 2001 From: jedi Date: Mon, 23 Oct 2023 15:12:26 +0200 Subject: [PATCH 04/12] add frontend skeleton --- frontend/.gitignore | 28 +++++++++++++++ frontend/index.html | 14 ++++++++ frontend/src/assets/icons/toolshed-48x48.png | Bin 0 -> 10967 bytes frontend/vite.config.js | 35 +++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 frontend/.gitignore create mode 100644 frontend/index.html create mode 100644 frontend/src/assets/icons/toolshed-48x48.png create mode 100644 frontend/vite.config.js diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..38adffa --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9a9af3d --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + + Toolshed + + +
+ + + diff --git a/frontend/src/assets/icons/toolshed-48x48.png b/frontend/src/assets/icons/toolshed-48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..6abc959e6f8122fa728eee0e5c43146c9fa3c2d9 GIT binary patch literal 10967 zcmeHtXH-+$)^_L}q)3w{RZ4(_gc3!12kBCj5>g=40HI5fCV~orqVz6BsuU?o?_Cg) zjz|Xq1(D7hJm;Q!?)cs@zH!HR@4uVuo$Ni=oX?u`nRBh3y>=cN+|r<-WTylG05n>f zsz&&4$%~zw6#r>Pc=!MSU}*9;F~=FfeR!}QZfJ}%iU;S1Me(3~F=zn5cd{fI#eGv|uK!34*QvY_DWSGKp0FVDU`IL;p*Sh-f#7Wj?FET zmE+EX!^g~XyW?&`{t`@8yh8~;(+l1UQJ(p=y%$;?s&RI$zBkkQdFboWXRW}XM?uv; z{odF^=gyu)td_KUh5ou;YkSCTHV~^ZOS4c19R`mmS4C5zY(vy zxIQnR>BiVmy+qbi>BgP*%NarQ9M3~NYl1qXz5L2-E2&Ypn&cims@)}p&H#e%1)m#P6S3(= zztPU6|NK4Gj{jE}MXMfeUXi@(CUmveUtck*7H0<%k0W@qtn4u{u@LC~MDaGmM~iy8 z4|L#pqaQJ9N36<%GA0Z!miannR04mzbN1x~^Dzo98g%PXr6D_BHtRpUO#PF|NLP5t z#7GbLlf$E5c;)UJ&59QW*}{d3M%uuVJ$A?9!evXxl9K)9J0TF`zLKg*%Z)TA@4@Lq z+)Bvo18#Wx)Bf(9N3h9N#)E#@(f-PQ*wkiUS^0eIb2AIC>an*|ikPl6?^)FNjJJZ~ zH)78PEujpLbxeLR`kUT{#N0WL)7C(8Q_{%&B`K$Omc3oVsjK|L_Wb=RtKGhalz=L; zL1!MmK%G*Nin*xS)dw1E3me{WdvckO*m~zzW zJ$Hb4w@<`KOh=2r=v$squHdi+%Gp%ize@N~u~-4!P{xp4%+P z_hL0!KPlpp_A`4`@yqKtc{6N#gtzlyyNek;seRv1YM)Pg`H$)*UHgXW2;gf@&$cEX z)>|8%)x3Wp{c$1i$K1_z)fIK%*cE8FQ)pPSiXwfX@zk<72faw}A~eAH3!igIM&rsQ z4_zQ8ok`jYP-Q>Z0{qlJ=y)&0Z)94(v`FGvH^Mtn{G|#>>FQ$3r*h_^n@UqwO9R56 zobP0`V(Bl>~6XwZF}GfT}nfU-G}ByeGeO*yiwjIcrI1? zw6=CXO%pA4)6y}0$T#{ygqP<$P=@T-ha@83n{UZT+WUSlt?im_1{c#4&aZ1t4+;*A z#VR}2Pn&sZ9U0NQyNu;7wl9zSU#ay<&~%~Ay6ZwEPe@P9<|nY9f87tv-*4-j6bMOW zl20sLB~crygo0O@3K04R#C@`v3e?#hd66@P<3iC9o}LSo15YRJd2qNItW4yu(m1aU zrDkneMv?j}zu&uYfV;EE!bnUa5VLG-#w8h2Fw(}n+uvNsT!T>?{ld1vLb0AeClp^E zBWWQXS@bohVJDVPRvXDl@|-8tgomKk$g1iRm*#XJ!)Pb%Q-K1?-iTqeoy}-0YqCU} z7jL*;2F)vn9zE%e(e@aoBJMcpb#Yp81xug82JI*Ny~sR+Lb9>`+7DscH+eJ<(iDBL zJ_f1U1zyH#BwnQ3ma^Bb-Uz#lX6-BmSoV?6{;7RhyX%)JMN;1+U`0q`Di8EH6hy|H{Q>EZ=MBwY z6a|Qk4uxeyVFivBPl@P|w1@)bytL4#^IxyUjaQDyA!%|vu5C(j-W3&BzAtLnI30*h z+>q#@cUnhj6F$)+4BZpZeBa+H@+p(TT*)cqm{N49NJR@hM0b@mgW-r?H9U{EW0aHV za^mBvw{(ZBk9`Su6!{j$sBa%cj;fQNgL%I*RCElkeP=x)t7L1{eL3P~Xi7_&Dv!yw zD^vd9lpaHxb>)U% z>K>(bm+MiCytbSE>*}|F7*8@IISo(0uTy1@OxX6?Fle*d`%1<(XV#1 zDA=>qSr$94Dc2btaf{Mwd?{z&5xf=V!h7(XG}6l)Yv)S%K-W8{F3;%^zgPYP-AaoJ zaCeStx5^AV9eE6d?eVRW+01a;T<-7j@Hb)T^O7I8-KiQ!an5y5XdPto^wYMg4@Pfw zW9apZ`yv|rcGBIR9Ot00m9k?Wo&eMbzhbivsV0RhwT~)In*wIEBOTu()4gckd82}H zAS&q+SDElWGzV5rluyGtjQUYhDy1bfgp;)wWwoJUN?7(b*8x`Zpkuoft7%Hx^H%Bb znMazGFo9(Wt%PCi4bvP+TXw?Ft0A|gA%>&jtiR#i-ZIH)>F3%7~7NZE3zq zLF-#dTolS0XZLbxi+S&wZ=d|k)eFhR;!v;_OlF87ymt{LgtmW8rzZFI_`=c z6RuU_?pBFT&|0-@bCrv-vvT%FKKB{h)}M9Ppj@R(_rJERbcTTqrG@IFKW^}O9{gIL z6teG3w5s<;^bWASCMGev*OU8DR*48|Y<&x?N5SO`p6yToUCkk6pk@6k6xpSdN6vS} zN0rrjiFkD6*|w(!Y98~N$V1$omeW)J4lP9{cb@)wF)@pV*)*!l1r)@{sn}DkZu)%_ zWNBK_qp1avKJ8#D2YJ&)PZIE$A!)zZ40AI1&X6DQt8<4(kCX0AT{j484W#cXj@jM$ zESVXW#&d@?I-Ilph~)cvE|6RJ=BIS!;xiY_s{oFQO){?^0EkZ!O^tf6hbZoTrd!hV z_oJ)B?zV2rtH*6rqbp-hkus;Q+)PyacPh1dYcngiO20AJe4J`N)tm-8J4U(8vlo%3 z}xs0 zQl+VW-ttb$?wlI%mFLgCMR$W+%9PjyARZ&5Bth`aVPk`J^(Cj+r{QbIhL|=Ioj|&; z6d4WABfkkiPG}5=?L)fnINh#ZPPbIyb^zJ;cp$YVkcC&m$Wyj8f5QY}22 zTi-=z)qSolR8GrUrxA51G1f7nu4>mHsZYbyN4@}`Bp%(b7SzzAD%iVm`{bby&0JgB z$hbqwxXb5h1x>KK0Ht|pnzp}Y{9|e{`McDf9|GV9LdT*`^Q+s3n3L#bm6Ulxjk#BD zFJGD~ydzahED^jr%#^#ea7L<^>Tp9SiMCS0K^*f)QZ)|oVH{|6cem-ZC2ps1X;0_I z^`Bg0!B`YD#y6&}UnkQ|cuk~Ylge9vgT5S_8vgT()vkcwqYtAo`?1z0&SMWA9=fOl zpO8t_Gb_(M(0&{AQrt{c0;k#f0$ z!&y8agdrcD+2lvM*(A#FtTCHTG+Z_#fdvU^b`nr-&ud7VqqcpobGm`i%N2L9sG$5% z$hd=Z)_<{-r?Z3NXHWoM8$IyaQCiEf@a1xDpD-6@Rv)NuZTO@UKSp#5h1{8$MSHS0omOp?g+R~MtUdtAzd6ikOi~U&BjG~PJT+%f|WgiVz6DviNZI?c)7Q_k9vhySOsjfwAQtrY;MiGM)b1VH)>MeKGxKYEkr>Nl{W8 zih>qQ9EN0l*R|w{ON4{^Q4RqLVOpbzHwsIar(#S$kgv}=DNZ-ug0iDslpk6xTM}Nn zq%FeEfiVhL3I8@AcJ@nACNvy9&B8$9zu_YizfM6fbM#sA$_V#d_>bC%mJUXHbYZ=| zvT+h%hXa~jw+UVkY69L=RLiZMBzepd;Sr~>8n;SEGf8-BsBZ(&QPH4WbLZf?!u>cBkU(61iRkZ0scIo;kP_^?TP-8rqc245_%R z%r3|cg#-k*;Nf~95Bb3S z#8%y28SkF!cI#?NKSa43OWU1oRbHzK4ub}uOl2IhkJDEIgtkAiBO~ZXr&r!qFY|4685&qSsh7o{K(YruJv?bm zhQ}xSJJY!1@(mX?+k42D9_-2*pFf9>-s2Upa(=At({IqvOye7QzdnDk!XtB3{xk(q zxSbXekguy;GjJX9>F(Z-6~S+EH;H4dlC(FRvPQkKA8-aR7GV~=NI(P*ijpYO68Yw55sBQsOFbOLbqayP;~ zWjKGqWZCYEosRYS#pSaN$TCGFU)Cl}c1@YUY4@|-?l?0erdKDYS4E-OJma6;-#)0< z2r2WSNM8!x8lpFiVJ0Hvb&qFzrT0$hMkVG->aT~0V&2#7EA5PeR&fVUH=?`TQv1ZI zZb+e*-|V)7>?)?)wCO6N3+eS4yG-@+?_J%l==F=KyzNvzD1S)Brxu7x+JI<0?c`15 zQ3?+wEh=bH551knL}f9le|VsAw@Fnz;;yNrhgc>p&6^Ff^RT%Y%e+D@3E_CcBWsQo z1oi41$)sM_;Z5(NK5xhn`Q~o3R-U7)d%#FwKR9{`an^1wbKNV8PpTctb{d< zuRRa>C3W3TV;Yc?-l(?@87QgZc0NhaU#ljIzC>)O+${yn?&%52w#LI(0 z%JZ5*^(6JMDkukxroRWu*#DLZ!ruuYhvZebNqNH;h6iv#;ov;JF3zr=FkgAz-?%XR z@r77|m*=+%&PkrvT+e_<#mxi70}+RaO9It=G2UR_o0L2^JdkLZk*fNi5co5BUI!cw z3zLxW@$nJ&krH?Fu$KVI$;nAbf+fIUAYKFL>F0`r`vP4(`7R*-z)(ebB0Mly9LCL+ z=K>RM=jMfz=jFw>^Zdg<7p$J%U+}J;f3kq*L&6u1l>muLO1QX4{N2J6r{;|Z`O~5Q z)xy&Rzd$Qtgz|Lr@<5=}yiu+=zQ02t5r5fZy*!+Mhl4~&pqx=IcvVk)ub_YFQbSA6 z;4g~{3hXg1*xy!ovj0UBhe7|7tbg(CqUCoue|H3L{ul1QX#XSj-^zF`Jw2GJ8^Y_t zJuOvv-i!WWNH+uq3HyBrmxDl%U?~Vt+D=9i2$6xy0qtZVl0YaNj)FqbNEs9w@;4|g zS5F+=6@j{d!h?%r@Hl8$IXR@P9Rdgfqwo+&I2>pvjgkUN+DS^uAta$lq@>*6AoM*j z_^O0E|J|z#C?p;V36YYLm6DbPB4ki-AjA$N1C*0Rfq+tIgscqI4vLie(;FlLrtaq9 z0>`Hl;{vxwNnl;=fA?`gI84bv3xAyvm;9&1z!{E1;|=6_buq49zW;16!MLD|aqtT^ zLDDj^Ucg8k{h z1)QE65`*^p|7l+g9-bR`eSFF_F`oGT{eFx77*WP3_dm}5ICaMS9!fkszXt^jj`$-4 zPq;S<`P)xC)*np>2e_*}3O{@NS+4((WB!*?fItu^2+|G(l#)S710e_mz7#+RC{RjL z7Gj4&!14V4QB8lLd%B@#LP3F06chr4fKW)F9ZFUfC?|=QM5AToAYhQ)zdHSY5aQoz z;?EFoNL);d|48Z$iT@|r7hhYUYIAgPz9V{yffeDC*UO%SWOF0{CWWFVka1Q`S>|rNQTqWQzKg;A}6L6$dj7~<0Uy- zs!Apfv)?Q*SmW`_XM=wF)=U=B44adnL}Vg3VhyL9kkzg$_&KTO5#xe$oZ^C7X&Mfu zn3)wI2u!#I!(hQyf-wk-NSLCa7)A%2G%uL|7S~M{uY!nbz5!B7r#xesQ1}0}{{V$h zkl&VGCi@ku4%<%mX$;st9P}v-d5u7m>Ak?;22VK(9&!N4wXPt7#!J_H>GU5Mr^wXa z*J`BXp;P2an1+|po}Bc~-PePUdEfkg2N0W_M3SK3eY1f%0Q}XAD<(`YRdcjq zy7PzB^}vF@CTs_wJRr>VDn=nhjnvrfht$l@F-XvY{imO@R60>oY-FP#KR1|kC~ie_ z7|Hs+)v>J6>3E;%{W-%{$X9 zp%|CqmYfoUra8>y6YK1`6p5q_tbqN>=j*hlRC@Szuw7RJt`re z7Jq3&HZS?I6(;I~lj%QO$tCzKsv;0t9srg|8tnLD_%>-LQENxzGW~fcGK?(bb6+}w zImWytG;75*eQI4SIC8WC7r$H(jvBk;4?0VrWhvALn zei0d?y-R=zIOK6BInUih12bWeLfzcP_V9*v)%K9#R9J%_+as1Y2;N zBXavZJA{1JyloUXs&#j9p@NtWV3;A4D>sG$SW>^DR&+&?x6?SjF^pL()?9m;Uh`|6 z@HHt^SWqQ#BKM3MP*s236&)oY9HFu5RJ|OkABniyAhUW4^l`~%m{(3TJXbwTV6eFR zC%_931^CRd+wiUpq~nutOr%-o-Y&rS1^|d!l+mNo=jhT~nPI!7Ls#{UIsRt*E9^RG zCV?&X(8?N+LZI5>U>|?wW5LiaS4e1uQP%RWceZ7eVpr4|=qxdE=#1L@DLYTx7BQ9u zAUo*2q7Eh@^UAT{9fHqvN@19qLJD|B5@m=_3GJ>Y`0eaDH z;|Q9uxc@4@iE3U{#XB2@%TenlBE-!&+{AS*Y?1y_KySK!H2gl{oCYK#=-1&&%FlTm zp-|x;W`in^(b{GXWdS22DjE=1Goq#a(qAT})CJT7r8UM8mi)(l8l8oA<^j@qrToKZFV)z9~i1zborNX>rqk!8k``UoT;%b0sCzSWkB z>W?oe0X*aNPsy5F`FA;0LZuz2thPUkgo$32D}~n0CMUh_2x3Q0UtjcJ+D#?9ER8kIA#aEbz3fFmWeQJ_=?+&n?HUx>*VQub1Xk^bpY-X_4jU=ae>{>0cOJ72OIu4OC6eE(7drgYI9-&T0}) zwgbS?@e0~OhrU|FUQS}kfuf2+4~Qk&dmU81G=$lw7iNq;&srDgv6O^Wca1po639B) z)z$9Vgq+%V5>~!(wmJ;yXJ0jB#FYRliApWFT(6>N0D9~45T7sgWXDpt z)m-M_EaSF!;gR54NdquT_6&TrI(+?g{bly|;7tap&;GDJ3j7ruprv+8wN%+Q^nU Date: Sun, 17 Mar 2024 18:52:41 +0100 Subject: [PATCH 05/12] frontend: add /login and /register forms --- frontend/package-lock.json | 2807 ++++++++++++++++++++++++ frontend/package.json | 29 + frontend/src/App.vue | 18 + frontend/src/components/BaseLayout.vue | 110 + frontend/src/components/Footer.vue | 53 + frontend/src/main.js | 16 + frontend/src/router.js | 35 + frontend/src/scss/_card.scss | 58 + frontend/src/scss/_forms.scss | 71 + frontend/src/scss/toolshed.scss | 138 ++ frontend/src/views/Index.vue | 31 + frontend/src/views/Login.vue | 106 + frontend/src/views/Register.vue | 163 ++ 13 files changed, 3635 insertions(+) create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/components/BaseLayout.vue create mode 100644 frontend/src/components/Footer.vue create mode 100644 frontend/src/main.js create mode 100644 frontend/src/router.js create mode 100644 frontend/src/scss/_card.scss create mode 100644 frontend/src/scss/_forms.scss create mode 100644 frontend/src/scss/toolshed.scss create mode 100644 frontend/src/views/Index.vue create mode 100644 frontend/src/views/Login.vue create mode 100644 frontend/src/views/Register.vue diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..e726ed1 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2807 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "bootstrap": "^4.6.2", + "bootstrap-icons-vue": "^1.10.3", + "dns-query": "^0.11.2", + "js-nacl": "^1.4.0", + "moment": "^2.29.4", + "vue": "^3.2.47", + "vue-multiselect": "^2.1.7", + "vue-router": "^4.1.6", + "vuex": "^4.1.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.0.0", + "@vue/test-utils": "^2.3.2", + "jsdom": "^22.0.0", + "sass": "^1.72.0", + "vite": "^4.1.4", + "vitest": "^0.31.1" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", + "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@leichtgewicht/base64-codec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@leichtgewicht/base64-codec/-/base64-codec-1.0.0.tgz", + "integrity": "sha512-0cgP4lRBzh3F4tlpTfs7F+PJyBN8j5yUC9KrQFWp/bREswgzZVHE8T1rNyRDWgvALwwpPtnJDQfqWUmxI33Epg==" + }, + "node_modules/@leichtgewicht/dns-packet": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@leichtgewicht/dns-packet/-/dns-packet-6.0.3.tgz", + "integrity": "sha512-qmVHhFBFiBvPsk/wJ/EdoWHb+tGkzY4haybmDPukhF6w0+8wpEbrHTIRE9LzeUu2P0bAbmrK8WOXt5V5QN6jQg==", + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.4", + "bytes.js": "^0.0.2", + "utf8-bytes": "^0.0.1", + "utf8-codec": "^1.0.0", + "utf8-length": "^0.0.1", + "utf8-string-bytes": "^1.0.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@leichtgewicht/dns-socket": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@leichtgewicht/dns-socket/-/dns-socket-5.0.0.tgz", + "integrity": "sha512-Sbrn/OG0HTTPGSkwIDCHy8/tUI6UglIzFsMNjzZn/Na1/i5owSm6rVi9CfKNNjRcUlYEzICELYW6EoZdjwVY2A==", + "dependencies": { + "@leichtgewicht/dns-packet": "^6.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/chai": { + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.14.tgz", + "integrity": "sha512-Wj71sXE4Q4AkGdG9Tvq1u/fquNz9EdG4LIJMwVVII7ashjD/8cf8fyIfJAjRr6YcsXnSE8cOGQPq1gqeR8z+3w==", + "dev": true + }, + "node_modules/@types/chai-subset": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.5.tgz", + "integrity": "sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/node": { + "version": "20.11.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", + "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", + "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitest/expect": { + "version": "0.31.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.31.4.tgz", + "integrity": "sha512-tibyx8o7GUyGHZGyPgzwiaPaLDQ9MMuCOrc03BYT0nryUuhLbL7NV2r/q98iv5STlwMgaKuFJkgBW/8iPKwlSg==", + "dev": true, + "dependencies": { + "@vitest/spy": "0.31.4", + "@vitest/utils": "0.31.4", + "chai": "^4.3.7" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "0.31.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.31.4.tgz", + "integrity": "sha512-Wgm6UER+gwq6zkyrm5/wbpXGF+g+UBB78asJlFkIOwyse0pz8lZoiC6SW5i4gPnls/zUcPLWS7Zog0LVepXnpg==", + "dev": true, + "dependencies": { + "@vitest/utils": "0.31.4", + "concordance": "^5.0.4", + "p-limit": "^4.0.0", + "pathe": "^1.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "0.31.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.31.4.tgz", + "integrity": "sha512-LemvNumL3NdWSmfVAMpXILGyaXPkZbG5tyl6+RQSdcHnTj6hvA49UAI8jzez9oQyE/FWLKRSNqTGzsHuk89LRA==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.0", + "pathe": "^1.1.0", + "pretty-format": "^27.5.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "0.31.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.31.4.tgz", + "integrity": "sha512-3ei5ZH1s3aqbEyftPAzSuunGICRuhE+IXOmpURFdkm5ybUADk+viyQfejNk6q8M5QGX8/EVKw+QWMEP3DTJDag==", + "dev": true, + "dependencies": { + "tinyspy": "^2.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "0.31.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.31.4.tgz", + "integrity": "sha512-DobZbHacWznoGUfYU8XDPY78UubJxXfMNY1+SUdOp1NsI34eopSA6aZMeaGu10waSOeYwE8lxrd/pLfT0RMxjQ==", + "dev": true, + "dependencies": { + "concordance": "^5.0.4", + "loupe": "^2.3.6", + "pretty-format": "^27.5.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.21.tgz", + "integrity": "sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==", + "dependencies": { + "@babel/parser": "^7.23.9", + "@vue/shared": "3.4.21", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz", + "integrity": "sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==", + "dependencies": { + "@vue/compiler-core": "3.4.21", + "@vue/shared": "3.4.21" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz", + "integrity": "sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==", + "dependencies": { + "@babel/parser": "^7.23.9", + "@vue/compiler-core": "3.4.21", + "@vue/compiler-dom": "3.4.21", + "@vue/compiler-ssr": "3.4.21", + "@vue/shared": "3.4.21", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.7", + "postcss": "^8.4.35", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz", + "integrity": "sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==", + "dependencies": { + "@vue/compiler-dom": "3.4.21", + "@vue/shared": "3.4.21" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.1.tgz", + "integrity": "sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==" + }, + "node_modules/@vue/reactivity": { + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.21.tgz", + "integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==", + "dependencies": { + "@vue/shared": "3.4.21" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.21.tgz", + "integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==", + "dependencies": { + "@vue/reactivity": "3.4.21", + "@vue/shared": "3.4.21" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz", + "integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==", + "dependencies": { + "@vue/runtime-core": "3.4.21", + "@vue/shared": "3.4.21", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.21.tgz", + "integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==", + "dependencies": { + "@vue/compiler-ssr": "3.4.21", + "@vue/shared": "3.4.21" + }, + "peerDependencies": { + "vue": "3.4.21" + } + }, + "node_modules/@vue/shared": { + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz", + "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==" + }, + "node_modules/@vue/test-utils": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.5.tgz", + "integrity": "sha512-oo2u7vktOyKUked36R93NB7mg2B+N7Plr8lxp2JBGwr18ch6EggFjixSCdIVVLkT6Qr0z359Xvnafc9dcKyDUg==", + "dev": true, + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/blueimp-md5": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", + "dev": true + }, + "node_modules/bootstrap": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", + "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "jquery": "1.9.1 - 3", + "popper.js": "^1.16.1" + } + }, + "node_modules/bootstrap-icons-vue": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/bootstrap-icons-vue/-/bootstrap-icons-vue-1.11.3.tgz", + "integrity": "sha512-Xba1GTDYon8KYSDTKiiAtiyfk4clhdKQYvCQPMkE58+F5loVwEmh0Wi+ECCfowNc9SGwpoSLpSkvg7rhgZBttw==" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes.js": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/bytes.js/-/bytes.js-0.0.2.tgz", + "integrity": "sha512-KrLm4hv5Qs9w6b0U7h1bCdqxrsf+e9QMsfHeyQFzAz94x/5Aqa+FTEUSNBtt5d2VuV3Hfiea3c4ti74RZDDYkg==" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/concordance": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz", + "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==", + "dev": true, + "dependencies": { + "date-time": "^3.1.0", + "esutils": "^2.0.3", + "fast-diff": "^1.2.0", + "js-string-escape": "^1.0.1", + "lodash": "^4.17.15", + "md5-hex": "^3.0.1", + "semver": "^7.3.2", + "well-known-symbols": "^2.0.0" + }, + "engines": { + "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dev": true, + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/date-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", + "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==", + "dev": true, + "dependencies": { + "time-zone": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dns-query": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/dns-query/-/dns-query-0.11.2.tgz", + "integrity": "sha512-zF8qxQpqCB467o4A63DLpQClo77H642JEKMx0Ra9GFww7Rx0234Fo8NoG0LBoSBZxamWkXfLxhzDG19bTBHvXQ==", + "dependencies": { + "@leichtgewicht/base64-codec": "^1.0.0", + "@leichtgewicht/dns-packet": "^6.0.2", + "@leichtgewicht/dns-socket": "^5.0.0", + "@leichtgewicht/ip-codec": "^2.0.4", + "utf8-codec": "^1.0.0" + }, + "bin": { + "dns-query": "bin/dns-query" + } + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "peer": true + }, + "node_modules/js-beautify": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", + "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", + "dev": true, + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.3.3", + "js-cookie": "^3.0.5", + "nopt": "^7.2.0" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-nacl": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/js-nacl/-/js-nacl-1.4.0.tgz", + "integrity": "sha512-HgYLcutGbMYBJrwgVICiHliuw1OJLy2U3tIuK6a1rZ06KC84TPl81WG1hcBRrBCiIIuBe3PSo9G4IZOMGdSg3Q==", + "engines": { + "node": "*" + } + }, + "node_modules/js-string-escape": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", + "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true + }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/md5-hex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", + "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==", + "dev": true, + "dependencies": { + "blueimp-md5": "^2.10.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", + "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mlly": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz", + "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.0.3", + "ufo": "^1.3.2" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nopt": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz", + "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==", + "dev": true, + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sass": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.72.0.tgz", + "integrity": "sha512-Gpczt3WA56Ly0Mn8Sl21Vj94s1axi9hDIzDFn9Ph9x3C3p4nNyvsqJoQyVXKou6cBlfFWEgRW4rT8Tb4i3XnVA==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", + "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "dev": true, + "dependencies": { + "acorn": "^8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/time-zone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", + "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tinybench": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", + "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.5.0.tgz", + "integrity": "sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/ufo": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", + "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", + "dev": true + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/utf8-bytes": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/utf8-bytes/-/utf8-bytes-0.0.1.tgz", + "integrity": "sha512-GifWmJAx2qAXT+lZLhbkWhBsy7pr6xWHiPWlVToDiELdWgZwt4Ogjf9tlgvKuALzTFR/d+EPQQI9ogJV3957Jg==" + }, + "node_modules/utf8-codec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utf8-codec/-/utf8-codec-1.0.0.tgz", + "integrity": "sha512-S/QSLezp3qvG4ld5PUfXiH7mCFxLKjSVZRFkB3DOjgwHuJPFDkInAXc/anf7BAbHt/D38ozDzL+QMZ6/7gsI6w==" + }, + "node_modules/utf8-length": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/utf8-length/-/utf8-length-0.0.1.tgz", + "integrity": "sha512-j/XH2ftofBiobnyApxlN/J6j/ixwT89WEjDcjT66d2i0+GIn9RZfzt8lpEXXE4jUe4NsjBSUq70kS2euQ4nnMw==" + }, + "node_modules/utf8-string-bytes": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/utf8-string-bytes/-/utf8-string-bytes-1.0.3.tgz", + "integrity": "sha512-i/I1Omf6lADjVBlwJpQifZOePV15snHny9w04+lc71+3t8PyWuLC/7clyoOSHOBNGXFe2PAGxmTiZ+Z4HWsPyw==" + }, + "node_modules/vite": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "0.31.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.31.4.tgz", + "integrity": "sha512-uzL377GjJtTbuc5KQxVbDu2xfU/x0wVjUtXQR2ihS21q/NK6ROr4oG0rsSkBBddZUVCwzfx22in76/0ZZHXgkQ==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.2.0", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "0.31.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.31.4.tgz", + "integrity": "sha512-GoV0VQPmWrUFOZSg3RpQAPN+LPmHg2/gxlMNJlyxJihkz6qReHDV6b0pPDcqFLNEPya4tWJ1pgwUNP9MLmUfvQ==", + "dev": true, + "dependencies": { + "@types/chai": "^4.3.5", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.31.4", + "@vitest/runner": "0.31.4", + "@vitest/snapshot": "0.31.4", + "@vitest/spy": "0.31.4", + "@vitest/utils": "0.31.4", + "acorn": "^8.8.2", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.7", + "concordance": "^5.0.4", + "debug": "^4.3.4", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.0", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "std-env": "^3.3.2", + "strip-literal": "^1.0.1", + "tinybench": "^2.5.0", + "tinypool": "^0.5.0", + "vite": "^3.0.0 || ^4.0.0", + "vite-node": "0.31.4", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*", + "playwright": "*", + "safaridriver": "*", + "webdriverio": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.4.21", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz", + "integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==", + "dependencies": { + "@vue/compiler-dom": "3.4.21", + "@vue/compiler-sfc": "3.4.21", + "@vue/runtime-dom": "3.4.21", + "@vue/server-renderer": "3.4.21", + "@vue/shared": "3.4.21" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.7.tgz", + "integrity": "sha512-7e12Evdll7JcTIocojgnCgwocX4WzIYStGClBQ+QuWPinZo/vQolv2EMq4a3lg16TKfwWafLimG77bxb56UauA==", + "dev": true + }, + "node_modules/vue-multiselect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-2.1.9.tgz", + "integrity": "sha512-nGEppmzhQQT2iDz4cl+ZCX3BpeNhygK50zWFTIRS+r7K7i61uWXJWSioMuf+V/161EPQjexI8NaEBdUlF3dp+g==", + "engines": { + "node": ">= 4.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.0.tgz", + "integrity": "sha512-dqUcs8tUeG+ssgWhcPbjHvazML16Oga5w34uCUmsk7i0BcnskoLGwjpa15fqMr2Fa5JgVBrdL2MEgqz6XZ/6IQ==", + "dependencies": { + "@vue/devtools-api": "^6.5.1" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vuex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.1.0.tgz", + "integrity": "sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==", + "dependencies": { + "@vue/devtools-api": "^6.0.0-beta.11" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/well-known-symbols": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz", + "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..fea4d8a --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "frontend", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "bootstrap": "^4.6.2", + "bootstrap-icons-vue": "^1.10.3", + "dns-query": "^0.11.2", + "js-nacl": "^1.4.0", + "moment": "^2.29.4", + "vue": "^3.2.47", + "vue-multiselect": "^2.1.7", + "vue-router": "^4.1.6", + "vuex": "^4.1.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.0.0", + "@vue/test-utils": "^2.3.2", + "jsdom": "^22.0.0", + "sass": "^1.72.0", + "vite": "^4.1.4", + "vitest": "^0.31.1" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..a7280b7 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,18 @@ + + + + + + + diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue new file mode 100644 index 0000000..75a836a --- /dev/null +++ b/frontend/src/components/BaseLayout.vue @@ -0,0 +1,110 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/Footer.vue b/frontend/src/components/Footer.vue new file mode 100644 index 0000000..93f2fac --- /dev/null +++ b/frontend/src/components/Footer.vue @@ -0,0 +1,53 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..358fd38 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,16 @@ +import {createApp} from 'vue' +import {BootstrapIconsPlugin} from 'bootstrap-icons-vue'; +import App from './App.vue' + +import './scss/toolshed.scss' + +import router from './router' + +import _nacl from 'js-nacl'; + +const app = createApp(App).use(BootstrapIconsPlugin); + +_nacl.instantiate((nacl) => { + window.nacl = nacl + app.use(router).mount('#app') +}); \ No newline at end of file diff --git a/frontend/src/router.js b/frontend/src/router.js new file mode 100644 index 0000000..1602093 --- /dev/null +++ b/frontend/src/router.js @@ -0,0 +1,35 @@ +import {createRouter, createWebHistory} from 'vue-router' +import Index from '@/views/Index.vue'; +import Login from '@/views/Login.vue'; +import Register from '@/views/Register.vue'; + + +const routes = [ + {path: '/', component: Index, meta: {requiresAuth: true}}, + {path: '/login', component: Login, meta: {requiresAuth: false}}, + {path: '/register', component: Register, meta: {requiresAuth: false}}, +] + +const router = createRouter({ + // 4. Provide the history implementation to use. We are using the hash history for simplicity here. + history: createWebHistory(), + linkActiveClass: "active", + routes, // short for `routes: routes` +}) + +router.beforeEach((to/*, from*/) => { + // instead of having to check every route record with + // to.matched.some(record => record.meta.requiresAuth) + if (to.meta.requiresAuth && false) { + // this route requires auth, check if logged in + // if not, redirect to login page. + console.log("Not logged in, redirecting to login page") + return { + path: '/login', + // save the location we were at to come back later + query: {redirect: to.fullPath}, + } + } +}) + +export default router \ No newline at end of file diff --git a/frontend/src/scss/_card.scss b/frontend/src/scss/_card.scss new file mode 100644 index 0000000..49d9171 --- /dev/null +++ b/frontend/src/scss/_card.scss @@ -0,0 +1,58 @@ + +.card { + margin-bottom: 24px; + box-shadow: 0 0 .875rem map-get($theme-colors, shadow); + background-clip: initial; + border: 0 solid transparent; +} + +.card-header { + background-color: map-get($theme-colors, background-1); + border-bottom: 0 solid transparent; +} + +.card-title { + color: map-get($theme-colors, text-3); + margin-bottom: .5rem; +} + +.card-subtitle { + margin-top: -.25rem; +} + +.card-subtitle, .card-text:last-child { + margin-bottom: 0; +} + + +.card { + & > .dataTables_wrapper .table.dataTable, + & > .table, + & > .table-responsive-lg .table, + & > .table-responsive-md .table, + & > .table-responsive-sm .table, + & > .table-responsive-xl .table, + & > .table-responsive .table { + border-right: 0; + border-bottom: 0; + border-left: 0; + margin-bottom: 0; + + & tr:first-child td, + & tr:first-child th { + border-top: 0; + } + + & td:last-child, + & th:last-child { + border-right: 0; + padding-right: 1.25rem; + } + + & td:first-child, + & th:first-child { + border-left: 0; + padding-left: 1.25rem; + } + } +} \ No newline at end of file diff --git a/frontend/src/scss/_forms.scss b/frontend/src/scss/_forms.scss new file mode 100644 index 0000000..73a0d4e --- /dev/null +++ b/frontend/src/scss/_forms.scss @@ -0,0 +1,71 @@ +.form-control { + width: 100%; + height: initial; + min-height: calc(1.8125rem + 2px); + padding: .25rem .7rem; + appearance: none; + background-color: initial; + border-radius: .2rem; + transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out; +} + +.form-control-lg { + height: initial; + min-height: calc(2.0875rem + 2px); + padding: .35rem 1rem; + font-size: .925rem; + border-radius: .3rem +} + +.btn { + display: inline-block; + font-weight: 400; + line-height: 1.5; + text-align: center; + vertical-align: middle; + cursor: pointer; + user-select: none; + padding: .25rem .7rem; + font-size: .875rem; + border-radius: .2rem; + transition: color .15s ease-in-out, background-color .15s ease-in-out, border-color .15s ease-in-out, box-shadow .15s ease-in-out; +} + +.form-select { + width: 100%; + padding: .25rem 1.7rem .25rem .7rem; + color: map-get($theme-colors, text-3); + background-color: map-get($theme-colors, background-1); + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right .7rem center; + background-size: 16px 12px; + border: 1px solid #ced4da; + border-radius: .2rem; + appearance: none; +} + +.btn-group-sm > .btn, .btn-sm { + padding: .15rem .5rem; + font-size: .75rem; + border-radius: .1rem; +} + +.input-group > :not(:first-child):not(.dropdown-menu) { + margin-left: -1px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.input-group > .dropdown-toggle:nth-last-child(n+3), .input-group > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group-text { + display: flex; + align-items: center; + padding: .25rem .7rem; + background-color: map-get($theme-colors, background-2); + border-right-width: 0; +} \ No newline at end of file diff --git a/frontend/src/scss/toolshed.scss b/frontend/src/scss/toolshed.scss new file mode 100644 index 0000000..160f891 --- /dev/null +++ b/frontend/src/scss/toolshed.scss @@ -0,0 +1,138 @@ +$variable-prefix: bs-; + +$white: #ffffff; + +$theme-colors: ( + "light": #d7e1dc, + "dark": #1f2327, + "primary": #3a7ddd, + "secondary": #45393a, + "info": #027980, + "success": #019a56, + "warning": #ffc107, + "danger": #ee1200, + "background-1": $white, + "background-2": #e9ecef, + "text-1": #000, + "text-3": #495057, + "shadow": #2125291a, +); + +$font-size-base: 0.875rem; + +$h1-font-size: $font-size-base * 2; +$h2-font-size: $font-size-base * 1.75; +$h3-font-size: $font-size-base * 1.5; +$h4-font-size: $font-size-base * 1.25; +$h5-font-size: $font-size-base; +$h6-font-size: $font-size-base; + +@import "bootstrap/scss/functions"; +@import "bootstrap/scss/variables"; + +$body-color: $gray-700; + +@import "bootstrap/scss/mixins"; +:root { + @each $color, $value in $colors { + --#{$variable-prefix}#{$color}: #{$value}; + } + + @each $color, $value in $theme-colors { + --#{$variable-prefix}#{$color}: #{$value}; + } + + @each $color, $value in $grays { + --#{$variable-prefix}gray-#{$color}: #{$value}; + } + + @each $bp, $value in $grid-breakpoints { + --#{$variable-prefix}breakpoint-#{$bp}: #{$value}; + } + + --#{$variable-prefix}font-family-sans-serif: #{inspect($font-family-sans-serif)}; + --#{$variable-prefix}font-family-monospace: #{inspect($font-family-monospace)}; +} + +@import "bootstrap/scss/reboot"; +@import "bootstrap/scss/type"; +@import "bootstrap/scss/images"; +@import "bootstrap/scss/code"; +@import "bootstrap/scss/grid"; +@import "bootstrap/scss/tables"; +@import "bootstrap/scss/forms"; +@import "bootstrap/scss/buttons"; +@import "bootstrap/scss/transitions"; +@import "bootstrap/scss/dropdown"; +@import "bootstrap/scss/button-group"; +@import "bootstrap/scss/input-group"; +@import "bootstrap/scss/custom-forms"; +@import "bootstrap/scss/nav"; +@import "bootstrap/scss/navbar"; +@import "bootstrap/scss/card"; +@import "bootstrap/scss/breadcrumb"; +@import "bootstrap/scss/pagination"; +@import "bootstrap/scss/badge"; +@import "bootstrap/scss/jumbotron"; +@import "bootstrap/scss/alert"; +@import "bootstrap/scss/progress"; +@import "bootstrap/scss/media"; +@import "bootstrap/scss/list-group"; +@import "bootstrap/scss/close"; +@import "bootstrap/scss/toasts"; +@import "bootstrap/scss/modal"; +@import "bootstrap/scss/tooltip"; +@import "bootstrap/scss/popover"; +@import "bootstrap/scss/carousel"; +@import "bootstrap/scss/spinners"; +@import "bootstrap/scss/utilities"; +@import "bootstrap/scss/print"; + +@import "card"; +@import "forms"; + +#root, body, html { + height: 100%; +} + +body { + overflow-y: scroll; + opacity: 1 !important; +} + +.main { + background-color: var(--bs-gray-300); +} + +.content { + padding: 1.5rem 1.5rem .75rem; + flex: 1; + width: 100vw; + max-width: 100vw; + direction: ltr +} + +@media (min-width: map-get($grid-breakpoints, md)) { + .content { + width: auto; + max-width: auto + } +} + +@media (min-width: map-get($grid-breakpoints, lg)) { + .content { + padding: 2.5rem 2.5rem 1rem + } +} + +.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { + font-weight: 400; + color: map-get($theme-colors, text-1); +} + +.table > :not(caption) > * > * { + padding: .75rem; + background-color: var(--bs-table-bg); + background-image: linear-gradient(var(--bs-table-accent-bg), var(--bs-table-accent-bg)); + border-bottom-width: 1px !important; +} \ No newline at end of file diff --git a/frontend/src/views/Index.vue b/frontend/src/views/Index.vue new file mode 100644 index 0000000..90fbeb1 --- /dev/null +++ b/frontend/src/views/Index.vue @@ -0,0 +1,31 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..7a2097b --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,106 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue new file mode 100644 index 0000000..f0091d0 --- /dev/null +++ b/frontend/src/views/Register.vue @@ -0,0 +1,163 @@ + + + + + \ No newline at end of file From f8dacef309acf52fe5f238e27c15c21a921c7c07 Mon Sep 17 00:00:00 2001 From: jedi Date: Mon, 11 Mar 2024 17:37:56 +0100 Subject: [PATCH 06/12] development setup using docker --- .gitignore | 3 +- deploy/dev/Dockerfile.backend | 16 +++++ deploy/dev/Dockerfile.dns | 16 +++++ deploy/dev/Dockerfile.frontend | 13 ++++ deploy/dev/Dockerfile.proxy | 14 ++++ deploy/dev/Dockerfile.wiki | 15 ++++ deploy/dev/dns_server.py | 72 +++++++++++++++++++ deploy/dev/instance_a/a.env | 8 +++ deploy/dev/instance_a/dns.json | 3 + deploy/dev/instance_a/domains.json | 3 + deploy/dev/instance_a/nginx-a.dev.conf | 96 ++++++++++++++++++++++++++ deploy/dev/instance_b/b.env | 7 ++ deploy/dev/instance_b/nginx-b.dev.conf | 46 ++++++++++++ deploy/dev/zone.json | 24 +++++++ deploy/docker-compose.override.yml | 78 +++++++++++++++++++++ frontend/node_modules/.forgit | 0 mkdocs.yml | 1 + 17 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 deploy/dev/Dockerfile.backend create mode 100644 deploy/dev/Dockerfile.dns create mode 100644 deploy/dev/Dockerfile.frontend create mode 100644 deploy/dev/Dockerfile.proxy create mode 100644 deploy/dev/Dockerfile.wiki create mode 100644 deploy/dev/dns_server.py create mode 100644 deploy/dev/instance_a/a.env create mode 100644 deploy/dev/instance_a/dns.json create mode 100644 deploy/dev/instance_a/domains.json create mode 100644 deploy/dev/instance_a/nginx-a.dev.conf create mode 100644 deploy/dev/instance_b/b.env create mode 100644 deploy/dev/instance_b/nginx-b.dev.conf create mode 100644 deploy/dev/zone.json create mode 100644 deploy/docker-compose.override.yml create mode 100644 frontend/node_modules/.forgit diff --git a/.gitignore b/.gitignore index 2e85c98..4ce5750 100644 --- a/.gitignore +++ b/.gitignore @@ -130,4 +130,5 @@ dmypy.json staticfiles/ userfiles/ -testdata.py \ No newline at end of file +testdata.py +*.sqlite3 diff --git a/deploy/dev/Dockerfile.backend b/deploy/dev/Dockerfile.backend new file mode 100644 index 0000000..4eb1ded --- /dev/null +++ b/deploy/dev/Dockerfile.backend @@ -0,0 +1,16 @@ +# Use an official Python runtime as instance_a parent image +FROM python:3.9 + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# Set work directory +WORKDIR /code + +# Install dependencies +COPY requirements.txt /code/ +RUN pip install --no-cache-dir -r requirements.txt + +# Run the application +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000", "--insecure"] diff --git a/deploy/dev/Dockerfile.dns b/deploy/dev/Dockerfile.dns new file mode 100644 index 0000000..055bdc0 --- /dev/null +++ b/deploy/dev/Dockerfile.dns @@ -0,0 +1,16 @@ +# Use an official Python runtime as instance_a parent image +FROM python:3.9 + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# Set work directory +WORKDIR /dns + +COPY dns_server.py /dns/ + +RUN pip install dnslib + +# Run the application +CMD ["python", "dns_server.py"] diff --git a/deploy/dev/Dockerfile.frontend b/deploy/dev/Dockerfile.frontend new file mode 100644 index 0000000..85138e6 --- /dev/null +++ b/deploy/dev/Dockerfile.frontend @@ -0,0 +1,13 @@ +# Use an official Node.js runtime as instance_a parent image +FROM node:14 + +# Set work directory +WORKDIR /app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +COPY package.json ./ + +RUN npm install + +CMD [ "npm", "run", "dev", "--", "--host"] diff --git a/deploy/dev/Dockerfile.proxy b/deploy/dev/Dockerfile.proxy new file mode 100644 index 0000000..095c80f --- /dev/null +++ b/deploy/dev/Dockerfile.proxy @@ -0,0 +1,14 @@ +FROM nginx:bookworm + +# snakeoil for localhost + +RUN apt-get update && \ + apt-get install -y openssl && \ + openssl genrsa -des3 -passout pass:x -out server.pass.key 2048 && \ + openssl rsa -passin pass:x -in server.pass.key -out server.key && \ + rm server.pass.key && \ + openssl req -new -key server.key -out server.csr \ + -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=localhost" && \ + openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt &&\ + mv server.crt /etc/nginx/nginx.crt && \ + mv server.key /etc/nginx/nginx.key \ diff --git a/deploy/dev/Dockerfile.wiki b/deploy/dev/Dockerfile.wiki new file mode 100644 index 0000000..affa564 --- /dev/null +++ b/deploy/dev/Dockerfile.wiki @@ -0,0 +1,15 @@ +# Use an official Python runtime as instance_a parent image +FROM python:3.9 + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# Set work directory +WORKDIR /wiki + +# Install dependencies +RUN pip install --no-cache-dir mkdocs + +# Run the application +CMD ["mkdocs", "serve", "--dev-addr=0.0.0.0:8001"] diff --git a/deploy/dev/dns_server.py b/deploy/dev/dns_server.py new file mode 100644 index 0000000..1457699 --- /dev/null +++ b/deploy/dev/dns_server.py @@ -0,0 +1,72 @@ +import http.server +import socketserver +import urllib.parse +import dnslib +import base64 + +try: + + def resolve(zone, qname, qtype): + for record in zone: + if record["name"] == qname and record["type"] == qtype and "value" in record: + return record["value"] + + + class DnsHttpRequestHandler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + try: + with open("/dns/zone.json", "r") as f: + import json + zone = json.load(f) + + url = urllib.parse.urlparse(self.path) + if url.path != "/dns-query": + self.send_response(404) + return + query = urllib.parse.parse_qs(url.query) + if "dns" not in query: + self.send_response(400) + return + query_base64 = query["dns"][0] + padded = query_base64 + "=" * (4 - len(query_base64) % 4) + raw = base64.b64decode(padded) + dns = dnslib.DNSRecord.parse(raw) + + response = dnslib.DNSRecord(dnslib.DNSHeader(id=dns.header.id, qr=1, aa=1, ra=1), q=dns.q) + + record = resolve(zone, dns.q.qname, dnslib.QTYPE[dns.q.qtype]) + if record: + if dns.q.qtype == dnslib.QTYPE.SRV: + print("SRV record") + reply = dnslib.SRV(record["priority"], record["weight"], record["port"], record["target"]) + response.add_answer(dnslib.RR(dns.q.qname, dns.q.qtype, rdata=reply)) + else: + response.header.rcode = dnslib.RCODE.NXDOMAIN + + print(response) + + self.send_response(200) + self.send_header("Content-type", "application/dns-message") + self.end_headers() + pack = response.pack() + self.wfile.write(pack) + return + except Exception as e: + print(f"Error: {e}") + self.send_response(500) + self.send_header("Content-type", "text/html") + self.end_headers() + self.wfile.write(b"Internal Server Error") + + + handler_object = DnsHttpRequestHandler + + PORT = 8053 + my_server = socketserver.TCPServer(("", PORT), handler_object) + + # Start the server + print(f"Starting server on port {PORT}") + my_server.serve_forever() + +except Exception as e: + print(f"Error: {e}") diff --git a/deploy/dev/instance_a/a.env b/deploy/dev/instance_a/a.env new file mode 100644 index 0000000..33c5b7e --- /dev/null +++ b/deploy/dev/instance_a/a.env @@ -0,0 +1,8 @@ + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG=True + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY='e*lm&*!j0_stqaiod$1zob(vs@aq6+n-i$1%!rek)_v9n^ue$3' + +ALLOWED_HOSTS="*" diff --git a/deploy/dev/instance_a/dns.json b/deploy/dev/instance_a/dns.json new file mode 100644 index 0000000..ead645f --- /dev/null +++ b/deploy/dev/instance_a/dns.json @@ -0,0 +1,3 @@ +[ + "127.0.0.3:5353" +] diff --git a/deploy/dev/instance_a/domains.json b/deploy/dev/instance_a/domains.json new file mode 100644 index 0000000..ad8f349 --- /dev/null +++ b/deploy/dev/instance_a/domains.json @@ -0,0 +1,3 @@ +[ + "a.localhost" +] diff --git a/deploy/dev/instance_a/nginx-a.dev.conf b/deploy/dev/instance_a/nginx-a.dev.conf new file mode 100644 index 0000000..b77f550 --- /dev/null +++ b/deploy/dev/instance_a/nginx-a.dev.conf @@ -0,0 +1,96 @@ +events {} + +http { + upstream backend { + server backend-a:8000; + } + + upstream frontend { + server frontend:5173; + } + + upstream wiki { + server wiki:8001; + } + + upstream dns { + server dns:8053; + } + + server { + + listen 8080 ssl; + server_name localhost; + + ssl_certificate /etc/nginx/nginx.crt; + ssl_certificate_key /etc/nginx/nginx.key; + + location /api { + proxy_set_header Host $host:$server_port; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host:$server_port; + proxy_set_header X-Forwarded-Port $server_port; + proxy_pass http://backend; + } + + location /auth { + proxy_set_header Host $host:$server_port; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host:$server_port; + proxy_set_header X-Forwarded-Port $server_port; + proxy_pass http://backend; + } + + location /docs { + proxy_pass http://backend/docs; + } + + location /static { + proxy_pass http://backend/static; + } + + location /wiki { + proxy_pass http://wiki/wiki; + } + + location /livereload { + proxy_pass http://wiki/livereload; + } + + location /local/ { + alias /var/www/; + try_files $uri.json =404; + add_header Content-Type application/json; + } + + location / { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_pass http://frontend; + } + + } + + # DoH server + server { + listen 5353 ssl; + server_name localhost; + + ssl_certificate /etc/nginx/nginx.crt; + ssl_certificate_key /etc/nginx/nginx.key; + + location /dns-query { + proxy_pass http://dns; + # allow any origin + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS'; + + } + } +} diff --git a/deploy/dev/instance_b/b.env b/deploy/dev/instance_b/b.env new file mode 100644 index 0000000..c0118ca --- /dev/null +++ b/deploy/dev/instance_b/b.env @@ -0,0 +1,7 @@ +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG=True + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY='7ccxjje%q@@0*z+r&-$fy3(rj9n)%$!sk-k++-&rb=_u(wpjbe' + +ALLOWED_HOSTS="*" diff --git a/deploy/dev/instance_b/nginx-b.dev.conf b/deploy/dev/instance_b/nginx-b.dev.conf new file mode 100644 index 0000000..bb6596c --- /dev/null +++ b/deploy/dev/instance_b/nginx-b.dev.conf @@ -0,0 +1,46 @@ +events {} + +http { + upstream backend { + server backend-b:8000; + } + + server { + + listen 8080 ssl; + server_name localhost; + + ssl_certificate /etc/nginx/nginx.crt; + ssl_certificate_key /etc/nginx/nginx.key; + + location /api { + #proxy_set_header X-Forwarded-For "$http_x_forwarded_for, $realip_remote_addr"; + proxy_set_header Host $host:$server_port; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host:$server_port; + proxy_set_header X-Forwarded-Port $server_port; + proxy_pass http://backend; + } + + location /auth { + #proxy_set_header X-Forwarded-For "$http_x_forwarded_for, $realip_remote_addr"; + proxy_set_header Host $host:$server_port; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host:$server_port; + proxy_set_header X-Forwarded-Port $server_port; + proxy_pass http://backend; + } + + location /docs { + proxy_pass http://backend/docs; + } + + location /static { + proxy_pass http://backend/static; + } + } +} diff --git a/deploy/dev/zone.json b/deploy/dev/zone.json new file mode 100644 index 0000000..5c8be9a --- /dev/null +++ b/deploy/dev/zone.json @@ -0,0 +1,24 @@ +[ + { + "name": "_toolshed-server._tcp.a.localhost.", + "type": "SRV", + "ttl": 60, + "value": { + "priority": 0, + "weight": 5, + "port": 8080, + "target": "127.0.0.1." + } + }, + { + "name": "_toolshed-server._tcp.b.localhost.", + "type": "SRV", + "ttl": 60, + "value": { + "priority": 0, + "weight": 5, + "port": 8080, + "target": "127.0.0.2." + } + } +] diff --git a/deploy/docker-compose.override.yml b/deploy/docker-compose.override.yml new file mode 100644 index 0000000..30b4de9 --- /dev/null +++ b/deploy/docker-compose.override.yml @@ -0,0 +1,78 @@ +version: '3.8' + +services: + backend-a: + build: + context: ../backend/ + dockerfile: ../deploy/dev/Dockerfile.backend + volumes: + - ../backend:/code + - ../deploy/dev/instance_a/a.env:/code/.env + - ../deploy/dev/instance_a/a.sqlite3:/code/db.sqlite3 + expose: + - 8000 + command: bash -c "python configure.py; python configure.py testdata; python manage.py runserver 0.0.0.0:8000 --insecure" + + backend-b: + build: + context: ../backend/ + dockerfile: ../deploy/dev/Dockerfile.backend + volumes: + - ../backend:/code + - ../deploy/dev/instance_b/b.env:/code/.env + - ../deploy/dev/instance_b/b.sqlite3:/code/db.sqlite3 + expose: + - 8000 + command: bash -c "python configure.py; python configure.py testdata; python manage.py runserver 0.0.0.0:8000 --insecure" + + frontend: + build: + context: ../frontend/ + dockerfile: ../deploy/dev/Dockerfile.frontend + volumes: + - ../frontend:/app:ro + - /app/node_modules + expose: + - 5173 + command: npm run dev -- --host + + wiki: + build: + context: ../ + dockerfile: deploy/dev/Dockerfile.wiki + volumes: + - ../mkdocs.yml:/wiki/mkdocs.yml + - ../docs:/wiki/docs + expose: + - 8001 + command: mkdocs serve --dev-addr=0.0.0.0:8001 + + proxy-a: + build: + context: ./ + dockerfile: dev/Dockerfile.proxy + volumes: + - ./dev/instance_a/nginx-a.dev.conf:/etc/nginx/nginx.conf:ro + - ./dev/instance_a/dns.json:/var/www/dns.json:ro + - ./dev/instance_a/domains.json:/var/www/domains.json:ro + ports: + - "127.0.0.1:8080:8080" + - "127.0.0.3:5353:5353" + + proxy-b: + build: + context: ./ + dockerfile: dev/Dockerfile.proxy + volumes: + - ./dev/instance_b/nginx-b.dev.conf:/etc/nginx/nginx.conf:ro + ports: + - "127.0.0.2:8080:8080" + + dns: + build: + context: ./dev/ + dockerfile: Dockerfile.dns + volumes: + - ./dev/zone.json:/dns/zone.json + expose: + - 8053 diff --git a/frontend/node_modules/.forgit b/frontend/node_modules/.forgit new file mode 100644 index 0000000..e69de29 diff --git a/mkdocs.yml b/mkdocs.yml index dadf43a..64c248f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,3 +24,4 @@ extra_javascript: - https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.js extra_css: - toolshed.css +site_url: https://localhost:8080/wiki/ From 48b9e595ff1ff0b8ab3f8cec3d3a4282dd5a8c99 Mon Sep 17 00:00:00 2001 From: jedi Date: Sun, 24 Mar 2024 06:44:38 +0100 Subject: [PATCH 07/12] frontend: add collapsable sidebar --- frontend/src/components/BaseLayout.vue | 7 +- frontend/src/components/Sidebar.vue | 181 +++++++++++++++++++++++++ frontend/src/main.js | 32 ++++- 3 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/Sidebar.vue diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue index 75a836a..21b3d75 100644 --- a/frontend/src/components/BaseLayout.vue +++ b/frontend/src/components/BaseLayout.vue @@ -1,5 +1,6 @@