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 4ce5750..b338100 100644 --- a/.gitignore +++ b/.gitignore @@ -130,5 +130,5 @@ dmypy.json staticfiles/ userfiles/ -testdata.py -*.sqlite3 +backend/templates/ +backend/testdata.py \ No newline at end of file diff --git a/README.md b/README.md index 7f91e3e..a2c0135 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # toolshed -## Development +## Installation / Development ``` bash git clone https://github.com/gr4yj3d1/toolshed.git @@ -12,10 +12,7 @@ or git clone https://git.neulandlabor.de/j3d1/toolshed.git ``` -all following development mode commands support auto-reloading and hot-reloading where applicable, they do not need to bw -restarted after changes. - -### Backend only +### Backend ``` bash cd toolshed/backend @@ -29,7 +26,7 @@ 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. -### Frontend only +### Frontend ``` bash cd toolshed/frontend @@ -37,45 +34,13 @@ npm install npm run dev ``` -### Docs only +### Docs ``` bash cd toolshed/docs mkdocs serve ``` -### Full stack - -``` bash -cd toolshed -docker-compose -f deploy/docker-compose.override.yml up --build -``` - -## Deployment - -### Requirements - -- python3 -- python3-pip -- python3-venv -- wget -- unzip -- nginx -- uwsgi - -### Installation - -* Get the latest release from -`https://git.neulandlabor.de/j3d1/toolshed/releases/download//toolshed.zip` or -`https://github.com/gr4yj3d1/toolshed/archive/refs/tags/.zip`. -* Unpack it to `/var/www` or wherever you want to install toolshed. -* Create a virtual environment and install the requirements. -* Then run the configuration script. -* Configure your webserver to serve the static files and proxy the requests to the backend. -* Configure your webserver to run the backend with uwsgi. - -for detailed instructions see [docs](/docs/deployment.md). - ## CLI Client ### Requirements diff --git a/backend/.idea/.gitignore b/backend/.idea/.gitignore index a9d7db9..13566b8 100644 --- a/backend/.idea/.gitignore +++ b/backend/.idea/.gitignore @@ -6,5 +6,3 @@ # Datasource local storage ignored files /dataSources/ /dataSources.local.xml -# GitHub Copilot persisted chat sessions -/copilot/chatSessions 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 e027805..4229da2 100755 --- a/backend/configure.py +++ b/backend/configure.py @@ -8,41 +8,32 @@ import dotenv from django.db import transaction, IntegrityError -class CmdCtx: +def yesno(prompt, default=False): + if not sys.stdin.isatty(): + return default + yes = {'yes', 'y', 'ye'} + no = {'no', 'n'} - def __init__(self, args): - self.args = args + if default: + yes.add('') + else: + no.add('') - def yesno(self, prompt, default=False): - if not sys.stdin.isatty() or self.args.noninteractive: - return default - elif self.args.yes: + hint = ' [Y/n] ' if default else ' [y/N] ' + + while True: + choice = input(prompt + hint).lower() + if choice in yes: return True - elif self.args.no: + elif choice in no: return False - yes = {'yes', 'y', 'ye'} - no = {'no', 'n'} - - if default: - yes.add('') else: - no.add('') - - hint = ' [Y/n] ' if default else ' [y/N] ' - - while True: - choice = input(prompt + hint).lower() - if choice in yes: - return True - elif choice in no: - return False - else: - print('Please respond with "yes" or "no"') + print('Please respond with "yes" or "no"') -def configure(ctx): +def configure(): if not os.path.exists('.env'): - if not ctx.yesno("the .env file does not exist, do you want to create it?", default=True): + if not yesno("the .env file does not exist, do you want to create it?", default=True): print('Aborting') exit(0) if not os.path.exists('.env.dist'): @@ -65,7 +56,7 @@ def configure(ctx): current_hosts = os.getenv('ALLOWED_HOSTS') print('Current ALLOWED_HOSTS: {}'.format(current_hosts)) - if ctx.yesno("Do you want to add ALLOWED_HOSTS?"): + if yesno("Do you want to add ALLOWED_HOSTS?"): hosts = input("Enter a comma-separated list of allowed hosts: ") joined_hosts = current_hosts + ',' + hosts if current_hosts else hosts dotenv.set_key('.env', 'ALLOWED_HOSTS', joined_hosts) @@ -76,29 +67,34 @@ def configure(ctx): django.setup() if not os.path.exists('db.sqlite3'): - if not ctx.yesno("No database found, do you want to create one?", default=True): + if not yesno("No database found, do you want to create one?", default=True): print('Aborting') exit(0) from django.core.management import call_command call_command('migrate') - if ctx.yesno("Do you want to create a superuser?"): + # 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 ctx.yesno("Do you want to import all categories, properties and tags contained in this repository?", - default=True): + 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] @@ -106,8 +102,6 @@ def configure(ctx): 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() @@ -122,13 +116,9 @@ def configure(ctx): 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]): - 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 + print('Not all dependencies for {} are imported, postponing'.format(name)) + queue.append(name) + continue else: print('unknown dependencies for {}: {}'.format(name, unmet_deps)) continue @@ -149,15 +139,10 @@ def configure(ctx): serializer = TagSerializer(data=tag) if serializer.is_valid(): serializer.save(origin=name) - imported_sets.create(name=name, hash=hashes[name]) + imported_sets.create(name=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 @@ -206,7 +191,6 @@ def main(): parser = ArgumentParser(description='Toolshed Server Configuration') parser.add_argument('--yes', '-y', help='Answer yes to all questions', action='store_true') parser.add_argument('--no', '-n', help='Answer no to all questions', action='store_true') - parser.add_argument('--noninteractive', '-x', help="Run in noninteractive mode", action='store_true') parser.add_argument('cmd', help='Command', default='configure', nargs='?') args = parser.parse_args() @@ -214,10 +198,8 @@ def main(): print('Error: --yes and --no are mutually exclusive') exit(1) - ctx = CmdCtx(args) - if args.cmd == 'configure': - configure(ctx) + configure() elif args.cmd == 'reset': reset() elif args.cmd == 'testdata': diff --git a/backend/hostadmin/admin.py b/backend/hostadmin/admin.py index 4dbabc7..84e6155 100644 --- a/backend/hostadmin/admin.py +++ b/backend/hostadmin/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import Domain, ImportedIdentifierSets +from .models import Domain class DomainAdmin(admin.ModelAdmin): @@ -9,11 +9,3 @@ 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 deleted file mode 100644 index d62713f..0000000 --- a/backend/hostadmin/migrations/0003_importedidentifiersets_hash.py +++ /dev/null @@ -1,39 +0,0 @@ -# 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 deleted file mode 100644 index 0d0404d..0000000 --- a/backend/hostadmin/migrations/0004_alter_importedidentifiersets_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# 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 da29811..bac6ac1 100644 --- a/backend/hostadmin/models.py +++ b/backend/hostadmin/models.py @@ -12,8 +12,4 @@ 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 02fadb6..72bacba 100644 --- a/backend/hostadmin/serializers.py +++ b/backend/hostadmin/serializers.py @@ -5,32 +5,6 @@ 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) @@ -38,21 +12,12 @@ 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 = 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) + parent = serializers.SlugRelatedField(slug_field='name', queryset=Category.objects.all(), required=False) class Meta: model = Category @@ -62,19 +27,7 @@ class CategorySerializer(serializers.ModelSerializer): class PropertySerializer(serializers.ModelSerializer): - 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) + category = serializers.SlugRelatedField(slug_field='name', queryset=Category.objects.all(), required=False) class Meta: model = Property @@ -85,19 +38,7 @@ class PropertySerializer(serializers.ModelSerializer): class TagSerializer(serializers.ModelSerializer): - 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) + category = serializers.SlugRelatedField(slug_field='name', queryset=Category.objects.all(), required=False) class Meta: model = Tag diff --git a/backend/hostadmin/tests.py b/backend/hostadmin/tests.py index 0df45c2..513d705 100644 --- a/backend/hostadmin/tests.py +++ b/backend/hostadmin/tests.py @@ -100,8 +100,7 @@ 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/subcat1", "cat1/subcat1/subcat2"]) + ["cat1", "cat2", "cat3", "cat1/subcat1", "cat1/subcat2", "cat1/subcat1/subcat3"]) def test_admin_get_categories_fail(self): response = client.get('/admin/categories/', self.f['local_user1']) @@ -110,7 +109,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()), 7) + self.assertEqual(len(response.json()), 6) self.assertEqual(response.json()[0]['name'], 'cat1') self.assertEqual(response.json()[1]['name'], 'cat2') self.assertEqual(response.json()[2]['name'], 'cat3') @@ -118,12 +117,10 @@ 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'], 'subcat1') - self.assertEqual(response.json()[5]['parent'], 'cat1/subcat1') - self.assertEqual(response.json()[6]['name'], 'subcat2') - self.assertEqual(response.json()[6]['parent'], 'cat1/subcat1') + self.assertEqual(response.json()[5]['name'], 'subcat3') + self.assertEqual(response.json()[5]['parent'], 'subcat1') - def test_admin_post_category(self): + def test_admin_create_category(self): response = client.post('/admin/categories/', self.f['admin'], {'name': 'cat4'}) self.assertEqual(response.status_code, 201) self.assertEqual(response.json()['name'], 'cat4') @@ -131,40 +128,6 @@ 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) @@ -173,18 +136,6 @@ 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) @@ -237,14 +188,6 @@ 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) @@ -307,13 +250,7 @@ class PropertyApiTestCase(UserTestMixin, CategoryTestMixin, PropertyTestMixin, T self.assertEqual(response.json()['base2_prefix'], False) self.assertEqual(response.json()['dimensions'], 1) - 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) + # self.assertEqual(response.json()['sort_lexicographically'], False) def test_admin_put_property(self): response = client.put('/admin/properties/1/', self.f['admin'], {'name': 'prop5'}) @@ -328,6 +265,8 @@ 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/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 aeefc0c..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, Category +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): @@ -12,24 +20,39 @@ admin.site.register(InventoryItem, InventoryItemAdmin) class PropertyAdmin(admin.ModelAdmin): - list_display = ('name', 'description', 'category', 'unit_symbol', 'base2_prefix', 'dimensions', 'origin') - search_fields = ('name', 'description', 'category', 'unit_symbol', 'base2_prefix', 'dimensions', 'origin') + list_display = ('name',) + search_fields = ('name',) admin.site.register(Property, PropertyAdmin) class TagAdmin(admin.ModelAdmin): - list_display = ('name', 'description', 'category', 'origin') - search_fields = ('name', 'description', 'category', 'origin') + list_display = ('name',) + search_fields = ('name',) admin.site.register(Tag, TagAdmin) - -class CategoryAdmin(admin.ModelAdmin): - list_display = ('name', 'description', 'parent', 'origin') - search_fields = ('name', 'description', 'parent', 'origin') +# 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) -admin.site.register(Category, CategoryAdmin) +# 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/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_alter_tag_options_alter_category_name_and_more.py b/backend/toolshed/migrations/0006_alter_tag_options_alter_category_name_and_more.py deleted file mode 100644 index 603a215..0000000 --- a/backend/toolshed/migrations/0006_alter_tag_options_alter_category_name_and_more.py +++ /dev/null @@ -1,67 +0,0 @@ -# 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/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 2dd0713..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,20 +9,60 @@ from authentication.models import ToolshedUser, KnownIdentity from files.models import File -class Category(SoftDeleteModel): +# @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) - parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, related_name='children') + parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=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 "" @@ -30,22 +72,17 @@ 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, related_name='properties') + category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=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) 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) 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 @@ -54,18 +91,9 @@ 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, related_name='tags') + category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=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 @@ -82,7 +110,8 @@ 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, related_name='inventory_items') + category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True, blank=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') @@ -109,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/fixtures.py b/backend/toolshed/tests/fixtures.py index 5ee9d8b..6b866cc 100644 --- a/backend/toolshed/tests/fixtures.py +++ b/backend/toolshed/tests/fixtures.py @@ -8,8 +8,7 @@ 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['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') + self.f['subcat3'] = Category.objects.create(name='subcat3', 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 d77a0eb..716b670 100644 --- a/backend/toolshed/tests/test_api.py +++ b/backend/toolshed/tests/test_api.py @@ -56,8 +56,7 @@ 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/subcat1', - 'cat1/subcat1/subcat2']) + ['cat1', 'cat2', 'cat3', 'cat1/subcat1', 'cat1/subcat2', 'cat1/subcat1/subcat3']) 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 10a552c..114e7ad 100644 --- a/backend/toolshed/tests/test_category.py +++ b/backend/toolshed/tests/test_category.py @@ -17,11 +17,10 @@ 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(), 2) + self.assertEqual(self.f['subcat1'].children.count(), 1) self.assertEqual(str(self.f['subcat1']), 'cat1/subcat1') self.assertEqual(str(self.f['subcat2']), 'cat1/subcat2') - self.assertEqual(str(self.f['subcat11']), 'cat1/subcat1/subcat1') - self.assertEqual(str(self.f['subcat12']), 'cat1/subcat1/subcat2') + self.assertEqual(str(self.f['subcat3']), 'cat1/subcat1/subcat3') class CategoryApiTestCase(CategoryTestMixin, UserTestMixin, ToolshedTestCase): @@ -34,12 +33,10 @@ 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()), 7) + self.assertEqual(len(reply.json()), 6) 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/subcat1') - self.assertEqual(reply.json()[6], 'cat1/subcat1/subcat2') - + self.assertEqual(reply.json()[5], 'cat1/subcat1/subcat3') diff --git a/backend/toolshed/tests/test_friend.py b/backend/toolshed/tests/test_friend.py index fba1a42..50751bf 100644 --- a/backend/toolshed/tests/test_friend.py +++ b/backend/toolshed/tests/test_friend.py @@ -369,6 +369,23 @@ 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): 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(), []) diff --git a/deploy/dev/Dockerfile.backend b/deploy/dev/Dockerfile.backend deleted file mode 100644 index 4eb1ded..0000000 --- a/deploy/dev/Dockerfile.backend +++ /dev/null @@ -1,16 +0,0 @@ -# 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 deleted file mode 100644 index 055bdc0..0000000 --- a/deploy/dev/Dockerfile.dns +++ /dev/null @@ -1,16 +0,0 @@ -# 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 deleted file mode 100644 index 85138e6..0000000 --- a/deploy/dev/Dockerfile.frontend +++ /dev/null @@ -1,13 +0,0 @@ -# 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 deleted file mode 100644 index 095c80f..0000000 --- a/deploy/dev/Dockerfile.proxy +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index affa564..0000000 --- a/deploy/dev/Dockerfile.wiki +++ /dev/null @@ -1,15 +0,0 @@ -# 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 deleted file mode 100644 index 1457699..0000000 --- a/deploy/dev/dns_server.py +++ /dev/null @@ -1,72 +0,0 @@ -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 deleted file mode 100644 index 33c5b7e..0000000 --- a/deploy/dev/instance_a/a.env +++ /dev/null @@ -1,8 +0,0 @@ - -# 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 deleted file mode 100644 index ead645f..0000000 --- a/deploy/dev/instance_a/dns.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - "127.0.0.3:5353" -] diff --git a/deploy/dev/instance_a/domains.json b/deploy/dev/instance_a/domains.json deleted file mode 100644 index ad8f349..0000000 --- a/deploy/dev/instance_a/domains.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - "a.localhost" -] diff --git a/deploy/dev/instance_a/nginx-a.dev.conf b/deploy/dev/instance_a/nginx-a.dev.conf deleted file mode 100644 index b77f550..0000000 --- a/deploy/dev/instance_a/nginx-a.dev.conf +++ /dev/null @@ -1,96 +0,0 @@ -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 deleted file mode 100644 index c0118ca..0000000 --- a/deploy/dev/instance_b/b.env +++ /dev/null @@ -1,7 +0,0 @@ -# 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 deleted file mode 100644 index bb6596c..0000000 --- a/deploy/dev/instance_b/nginx-b.dev.conf +++ /dev/null @@ -1,46 +0,0 @@ -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 deleted file mode 100644 index 5c8be9a..0000000 --- a/deploy/dev/zone.json +++ /dev/null @@ -1,24 +0,0 @@ -[ - { - "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 deleted file mode 100644 index 30b4de9..0000000 --- a/deploy/docker-compose.override.yml +++ /dev/null @@ -1,78 +0,0 @@ -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/docs/deployment.md b/docs/deployment.md deleted file mode 100644 index 9262ebe..0000000 --- a/docs/deployment.md +++ /dev/null @@ -1,102 +0,0 @@ -# Deployment - -## Native - -### Requirements - -- python3 -- python3-pip -- python3-venv -- wget -- unzip -- nginx -- uwsgi -- certbot - -### Installation - -Get the latest release: - -``` bash -cd /var/www # or wherever you want to install toolshed -wget https://git.neulandlabor.de/j3d1/toolshed/releases/download//toolshed.zip -``` -or from github: -``` bash -cd /var/www # or wherever you want to install toolshed -wget https://github.com/gr4yj3d1/toolshed/archive/refs/tags/.zip -O toolshed.zip -``` - -Extract and configure the backend: - -``` bash -unzip toolshed.zip -cd toolshed/backend -python3 -m venv venv -source venv/bin/activate -pip install -r requirements.txt -python configure.py -``` - -Configure uWSGI to serve the backend locally: - -``` bash -cd /var/www/toolshed/backend -cp toolshed.ini /etc/uwsgi/apps-available/ -ln -s /etc/uwsgi/apps-available/toolshed.ini /etc/uwsgi/apps-enabled/ -systemctl restart uwsgi -``` - -Configure nginx to serve the static files and proxy the requests to the backend: - -``` bash -cd /var/www/toolshed/backend -cp toolshed.nginx /etc/nginx/sites-available/toolshed -ln -s /etc/nginx/sites-available/toolsheed /etc/nginx/sites-enabled/ -systemctl restart nginx -``` - -Configure certbot to get a certificate for the domain: - -``` bash -certbot --nginx -d -``` - -### Update - -``` bash -cd /var/www -wget https://git.neulandlabor.de/j3d1/toolshed/releases/download//toolshed.zip -unzip toolshed.zip -cd toolshed/backend -source venv/bin/activate -pip install -r requirements.txt -python configure.py -systemctl restart uwsgi -``` - -## Docker - -### Requirements - -- docker -- docker-compose -- git - -### Installation - -``` bash -git clone https://git.neulandlabor.de/j3d1/toolshed.git -# or -git clone https://github.com/gr4yj3d1/toolshed.git -cd toolshed -docker-compose -f deploy/docker-compose.prod.yml up -d --build -``` - -### Update - -``` bash -toolshed -git pull -docker-compose -f deploy/docker-compose.prod.yml up -d --build -``` \ No newline at end of file diff --git a/docs/development.md b/docs/development.md deleted file mode 100644 index 3fcd229..0000000 --- a/docs/development.md +++ /dev/null @@ -1,105 +0,0 @@ -# Development - -``` bash -git clone https://github.com/gr4yj3d1/toolshed.git -``` - -or - -``` bash -git clone https://git.neulandlabor.de/j3d1/toolshed.git -``` - -## Native - -To a certain extent, the frontend and backend can be developed independently. The frontend is a Vue.js project and the -backend is a DRF (Django-Rest-Framework) project. If you want to develop the frontend, you can do so without the backend -and vice -versa. However, especially for the frontend, it is recommended to use the backend as well, as the frontend does not have -a lot of 'offline' functionality. -If you want to run the fullstack application, it is recommended to use the [docker-compose](#docker) method. - -### Frontend - -install `node.js` and `npm` - -on Debian* for example: `sudo apt install npm` - -``` bash -cd toolshed/frontend -npm install -npm run dev -``` - -### Backend - -Install `python3`, `pip` and `virtualenv` - -on Debian* for example: `sudo apt install python3 python3-pip python3-venv` - -Prepare backend environment - -``` bash -cd toolshed/backend -python -m venv venv -source venv/bin/activate -pip install -r requirements.txt -``` - -Run the test suite: - -``` bash -python manage.py test -``` - -optionally with coverage: - -``` bash -coverage run manage.py test -coverage report -``` - -Start the backend in development mode: - -``` bash -python manage.py migrate -cp .env.dist .env -echo "DEBUG = True" >> .env -python manage.py runserver 0.0.0.0:8000 -``` - -provides the api docs at `http://localhost:8000/docs/` - -### Docs (Wiki) - -Install `mkdocs` - -on Debian* for example: `sudo apt install mkdocs` - -Start the docs server: - -``` bash -cd toolshed/docs -mkdocs serve -a 0.0.0.0:8080 -``` - -## Docker - -### Fullstack - -Install `docker` and `docker-compose` - -on Debian* for example: `sudo apt install docker.io docker-compose` - -Start the fullstack application: - -``` bash -docker-compose -f deploy/docker-compose.override.yml up --build -``` - -This will start an instance of the frontend and wiki, a limited DoH (DNS over HTTPS) server and **two** instances of the backend. -The two backend instances are set up to use the domains `a.localhost` and `b.localhost`, the local DoH -server is used to direct the frontend to the correct backend instance. -The frontend is configured to act as if it was served from the domain `a.localhost`. -Access the frontend at `http://localhost:8080/`, backend at `http://localhost:8080/api/`, api docs -at `http://localhost:8080/docs/` and the wiki at `http://localhost:8080/wiki/`. \ No newline at end of file diff --git a/docs/federation.md b/docs/federation.md deleted file mode 100644 index d242677..0000000 --- a/docs/federation.md +++ /dev/null @@ -1,23 +0,0 @@ -# Federation - -This section will cover how federation works in Toolshed. - -## What is Federation? - -Since user of Toolshed you can search and interact the inventory of all their 'friends' that are potentially on -different servers there is a need for a way to communicate between servers. We don't want to rely on a central server that -stores all the data and we don't want to have a central server that handles all the communication between servers. This -is where federation comes in. Toolshed uses a protocol that can not only exchange data with the server where the user -is registered but also with the servers where their friends are registered. - -## How does it work? - -Any user can register on any server and creates a personal key pair. The public key is stored on the server and the private -key is stored on the client. The private key is used to sign all requests to the server and the public key is used to -verify the signature. Once a user has registered on a server they can send friend requests to other users containing -their public key. If the other user accepts the friend request, the server stores the public key of the friend and -uses it to verify access to the friend's inventory. While accepting a friend request the user also automatically sends -their own public key to the friend's server. This way both users can access each other's inventory. - -The protocol is based on a simple HTTPS API exchanging JSON data that is signed with the user's private key. By default -Toolshed servers provide a documentation of the API at [/docs/api](/docs/api). \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 95588dd..0ba4a97 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,8 +6,47 @@ This is the documentation for the Toolshed project. It is a work in progress. `#social` `#network` `#federation` `#decentralized` `#federated` `#socialnetwork` `#fediverse` `#community` `#hashtags` ## Getting Started - - [Deploying Toolshed](deployment.md) - - [Development Setup](development.md) - - [About Federation](federation.md) +## Installation +``` bash + # TODO add installation instructions + # similar to development instructions just with more docker + # TODO add docker-compose.yml +``` + +## Development + +``` bash +git clone https://github.com/gr4yj3d1/toolshed.git +``` +or +``` bash +git clone https://git.neulandlabor.de/j3d1/toolshed.git +``` + +### Frontend + +``` bash +cd toolshed/frontend +npm install +npm run dev +``` + +### Backend + +``` bash +cd toolshed/backend +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +python manage.py migrate +python manage.py runserver 0.0.0.0:8000 +``` + +### Docs + +``` bash +cd toolshed/docs +mkdocs serve -a 0.0.0.0:8080 +``` diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index 38adffa..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,28 +0,0 @@ -# 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 deleted file mode 100644 index 9a9af3d..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - Toolshed - - -
- - - diff --git a/frontend/node_modules/.forgit b/frontend/node_modules/.forgit deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index e726ed1..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,2807 +0,0 @@ -{ - "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 deleted file mode 100644 index fea4d8a..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "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 deleted file mode 100644 index a7280b7..0000000 --- a/frontend/src/App.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/frontend/src/assets/icons/toolshed-48x48.png b/frontend/src/assets/icons/toolshed-48x48.png deleted file mode 100644 index 6abc959..0000000 Binary files a/frontend/src/assets/icons/toolshed-48x48.png and /dev/null differ diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue deleted file mode 100644 index 21b3d75..0000000 --- a/frontend/src/components/BaseLayout.vue +++ /dev/null @@ -1,115 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/src/components/Footer.vue b/frontend/src/components/Footer.vue deleted file mode 100644 index 93f2fac..0000000 --- a/frontend/src/components/Footer.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/src/components/Sidebar.vue b/frontend/src/components/Sidebar.vue deleted file mode 100644 index 8643cbd..0000000 --- a/frontend/src/components/Sidebar.vue +++ /dev/null @@ -1,181 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/src/dns.js b/frontend/src/dns.js deleted file mode 100644 index f07ea22..0000000 --- a/frontend/src/dns.js +++ /dev/null @@ -1,50 +0,0 @@ -import {query} from 'dns-query'; - -function get_prefered_server() { - try { - const servers = JSON.parse(localStorage.getItem('dns-servers')); - if (servers && servers.length > 0) { - return servers; - } - } catch (e) { - console.error(e); - } - const request = new XMLHttpRequest(); - request.open('GET', '/local/dns', false); - request.send(null); - if (request.status === 200) { - const servers = JSON.parse(request.responseText); - if (servers && servers.length > 0) { - return servers; - } - } - return ['1.1.1.1', '8.8.8.8']; -} - -class FallBackResolver { - constructor() { - this._servers = get_prefered_server(); - this._cache = JSON.parse(localStorage.getItem('dns-cache')) || {}; - } - - async query(domain, type) { - const key = domain + ':' + type; - if (key in this._cache && this._cache[key].time > Date.now() - 1000 * 60 * 60) { - const age_seconds = Math.ceil(Date.now() / 1000 - this._cache[key].time / 1000); - return [this._cache[key].data]; - } - const result = await query( - {question: {type: type, name: domain}}, - { - endpoints: this._servers, - } - ) - if (result.answers.length === 0) throw new Error('No answer'); - const first = result.answers[0]; - this._cache[key] = {time: Date.now(), ...first}; // TODO hadle multiple answers - localStorage.setItem('dns-cache', JSON.stringify(this._cache)); - return [first.data]; - } -} - -export default FallBackResolver; \ No newline at end of file diff --git a/frontend/src/federation.js b/frontend/src/federation.js deleted file mode 100644 index 9afe33c..0000000 --- a/frontend/src/federation.js +++ /dev/null @@ -1,324 +0,0 @@ -class ServerSet { - constructor(servers, unreachable_neighbors) { - if (!servers || !Array.isArray(servers)) { - throw new Error('no servers') - } - if (!unreachable_neighbors || typeof unreachable_neighbors.queryUnreachable !== 'function' || typeof unreachable_neighbors.unreachable !== 'function') { - throw new Error('no unreachable_neighbors') - } - this.servers = [...new Set(servers)] // deduplicate - this.unreachable_neighbors = unreachable_neighbors; - } - - add(server) { - console.log('adding server', server) - if (!server || typeof server !== 'string') { - throw new Error('server must be a string') - } - if (server in this.servers) { - console.log('server already in set', server) - return - } - this.servers.push(server); - } - - async get(auth, target) { - if (!auth || typeof auth.buildAuthHeader !== 'function') { - throw new Error('no auth') - } - for (const server of this.servers) { - try { - if (this.unreachable_neighbors.queryUnreachable(server)) { - continue - } - const url = "https://" + server + target // TODO https - return await fetch(url, { - method: 'GET', - headers: { - ...auth.buildAuthHeader(url) - }, - credentials: 'omit' - }).catch(err => { - console.error('get from server failed', server, err) - this.unreachable_neighbors.unreachable(server) - } - ).then(response => response.json()) - } catch (e) { - console.error('get from server failed', server, e) - } - } - throw new Error('all servers failed') - } - - async post(auth, target, data) { - if (!auth || typeof auth.buildAuthHeader !== 'function') { - throw new Error('no auth') - } - for (const server of this.servers) { - try { - if (this.unreachable_neighbors.queryUnreachable(server)) { - continue - } - const url = "https://" + server + target // TODO https - return await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...auth.buildAuthHeader(url, data) - }, - credentials: 'omit', - body: JSON.stringify(data) - }).catch(err => { - console.error('post to server failed', server, err) - this.unreachable_neighbors.unreachable(server) - } - ).then(response => response.json()) - } catch (e) { - console.error('post to server failed', server, e) - } - } - throw new Error('all servers failed') - } - - async patch(auth, target, data) { - if (!auth || typeof auth.buildAuthHeader !== 'function') { - throw new Error('no auth') - } - for (const server of this.servers) { - try { - if (this.unreachable_neighbors.queryUnreachable(server)) { - continue - } - const url = "https://" + server + target // TODO https - return await fetch(url, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - ...auth.buildAuthHeader(url, data) - }, - credentials: 'omit', - body: JSON.stringify(data) - }).catch(err => { - console.error('patch to server failed', server, err) - this.unreachable_neighbors.unreachable(server) - } - ).then(response => response.json()) - } catch (e) { - console.error('patch to server failed', server, e) - } - } - throw new Error('all servers failed') - } - - async put(auth, target, data) { - if (!auth || typeof auth.buildAuthHeader !== 'function') { - throw new Error('no auth') - } - for (const server of this.servers) { - try { - if (this.unreachable_neighbors.queryUnreachable(server)) { - continue - } - const url = "https://" + server + target // TODO https - return await fetch(url, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - ...auth.buildAuthHeader(url, data) - }, - credentials: 'omit', - body: JSON.stringify(data) - }).catch(err => { - console.error('put to server failed', server, err) - this.unreachable_neighbors.unreachable(server) - } - ).then(response => response.json()) - } catch (e) { - console.error('put to server failed', server, e) - } - } - throw new Error('all servers failed') - } - - async delete(auth, target) { - if (!auth || typeof auth.buildAuthHeader !== 'function') { - throw new Error('no auth') - } - for (const server of this.servers) { - try { - if (this.unreachable_neighbors.queryUnreachable(server)) { - continue - } - const url = "https://" + server + target // TODO https - return await fetch(url, { - method: 'DELETE', - headers: { - ...auth.buildAuthHeader(url) - }, - credentials: 'omit' - }).catch(err => { - console.error('delete from server failed', server, err) - this.unreachable_neighbors.unreachable(server) - } - ) - } catch (e) { - console.error('delete from server failed', server, e) - } - } - throw new Error('all servers failed') - } - - async getRaw(auth, target) { - if (!auth || typeof auth.buildAuthHeader !== 'function') { - throw new Error('no auth') - } - for (const server of this.servers) { - try { - if (this.unreachable_neighbors.queryUnreachable(server)) { - continue - } - const url = "https://" + server + target // TODO https - return await fetch(url, { - method: 'GET', - headers: { - ...auth.buildAuthHeader(url) - }, - credentials: 'omit' - }).catch(err => { - console.error('get from server failed', server, err) - this.unreachable_neighbors.unreachable(server) - } - ) - } catch (e) { - console.error('get from server failed', server, e) - } - } - throw new Error('all servers failed') - } -} - -class ServerSetUnion { - constructor(serverSets) { - if (!serverSets || !Array.isArray(serverSets)) { - throw new Error('no serverSets') - } - this.serverSets = serverSets; - } - - add(serverset) { - if (!serverset || !(serverset instanceof ServerSet)) { - throw new Error('no serverset') - } - if (this.serverSets.find(s => serverset.servers.every(s2 => s.servers.includes(s2)))) { - console.warn('serverset already in union', serverset) - return - } - this.serverSets.push(serverset) - } - - async get(auth, target) { - try { - return await this.serverSets.reduce(async (acc, serverset) => { - return acc.then(async (acc) => { - return acc.concat(await serverset.get(auth, target)) - }) - }, Promise.resolve([])) - } catch (e) { - throw new Error('all servers failed') - } - } - - async post(auth, target, data) { - try { - return await this.serverSets.reduce(async (acc, serverset) => { - return acc.then(async (acc) => { - return acc.concat(await serverset.post(auth, target, data)) - }) - }, Promise.resolve([])) - } catch (e) { - throw new Error('all servers failed') - } - } - - async patch(auth, target, data) { - try { - return await this.serverSets.reduce(async (acc, serverset) => { - return acc.then(async (acc) => { - return acc.concat(await serverset.patch(auth, target, data)) - }) - }, Promise.resolve([])) - } catch (e) { - throw new Error('all servers failed') - } - } - - async put(auth, target, data) { - try { - return await this.serverSets.reduce(async (acc, serverset) => { - return acc.then(async (acc) => { - return acc.concat(await serverset.put(auth, target, data)) - }) - }, Promise.resolve([])) - } catch (e) { - throw new Error('all servers failed') - } - } - - async delete(auth, target) { - try { - return await this.serverSets.reduce(async (acc, serverset) => { - return acc.then(async (acc) => { - return acc.concat(await serverset.delete(auth, target)) - }) - }, Promise.resolve([])) - } catch (e) { - throw new Error('all servers failed') - } - } -} - - -class authMethod { - constructor(method, auth) { - this.method = method; - this.auth = auth; - } - - buildAuthHeader(url, data) { - return this.method(this.auth, {url, data}) - } - -} - -function createSignAuth(username, signKey) { - const context = {username, signKey} - if (!context.signKey || !context.username || typeof context.username !== 'string' - || !(context.signKey instanceof Uint8Array) || context.signKey.length !== 64) { - throw new Error('no signKey or username') - } - return new authMethod(({signKey, username}, {url, data}) => { - const json = JSON.stringify(data) - const signature = nacl.crypto_sign_detached(nacl.encode_utf8(url + (data ? json : "")), signKey) - return {'Authorization': 'Signature ' + username + ':' + nacl.to_hex(signature)} - }, context) -} - -function createTokenAuth(token) { - const context = {token} - if (!context.token) { - throw new Error('no token') - } - return new authMethod(({token}, {url, data}) => { - return {'Authorization': 'Token ' + token} - }, context) -} - -function createNullAuth() { - return new authMethod(() => { - return {} - }, {}) -} - -export {ServerSet, ServerSetUnion, createSignAuth, createTokenAuth, createNullAuth}; - - diff --git a/frontend/src/main.js b/frontend/src/main.js deleted file mode 100644 index 2277ac2..0000000 --- a/frontend/src/main.js +++ /dev/null @@ -1,47 +0,0 @@ -import {createApp} from 'vue' -import {BootstrapIconsPlugin} from 'bootstrap-icons-vue'; -import App from './App.vue' - -import './scss/toolshed.scss' - -import router from './router' -import store from './store'; - -import _nacl from 'js-nacl'; - -const app = createApp(App).use(store).use(BootstrapIconsPlugin); - -_nacl.instantiate((nacl) => { - window.nacl = nacl - app.use(router).mount('#app') -}); - -window.closeAllDropdowns = function () { - const dropdowns = document.getElementsByClassName("dropdown-menu"); - let i; - for (i = 0; i < dropdowns.length; i++) { - const openDropdown = dropdowns[i]; - if (openDropdown.classList.contains('show')) { - openDropdown.classList.remove('show'); - } - } -} - -window.onclick = function (event) { - if (!event.target.matches('.dropdown-toggle *') - && !event.target.matches('.dropdown-toggle') - && !event.target.matches('.dropdown-menu *') - && !event.target.matches('.dropdown-menu')) { - closeAllDropdowns(); - } - if (!event.target.matches('.sidebar-toggle *') - && !event.target.matches('.sidebar-toggle') - && !event.target.matches('.sidebar *') - && !event.target.matches('.sidebar')) { - const sidebar = document.getElementById("sidebar"); - const marginLeft = parseInt(getComputedStyle(sidebar).marginLeft); - if (sidebar.classList.contains('collapsed') && marginLeft === 0) { - sidebar.classList.remove('collapsed'); - } - } -} \ No newline at end of file diff --git a/frontend/src/neigbors.js b/frontend/src/neigbors.js deleted file mode 100644 index bbdfc06..0000000 --- a/frontend/src/neigbors.js +++ /dev/null @@ -1,48 +0,0 @@ -class NeighborsCache { - constructor() { - //this._max_age = 1000 * 60 * 60; // 1 hour - //this._max_age = 1000 * 60 * 5; // 5 minutes - this._max_age = 1000 * 15; // 15 seconds - this._cache = JSON.parse(localStorage.getItem('neighbor-cache')) || {}; - } - - reachable(domain) { - console.log('reachable neighbor ' + domain) - if (domain in this._cache) { - delete this._cache[domain]; - localStorage.setItem('neighbor-cache', JSON.stringify(this._cache)); - } - } - - unreachable(domain) { - console.log('unreachable neighbor ' + domain) - this._cache[domain] = {time: Date.now()}; - localStorage.setItem('neighbor-cache', JSON.stringify(this._cache)); - } - - queryUnreachable(domain) { - //return false if unreachable - if (domain in this._cache) { - if (this._cache[domain].time > Date.now() - this._max_age) { - console.log('skip unreachable neighbor ' + domain + ' ' + Math.ceil( - Date.now()/1000 - this._cache[domain].time/1000) + 's/' + Math.ceil(this._max_age/1000) + 's') - return true - } else { - delete this._cache[domain]; - localStorage.setItem('neighbor-cache', JSON.stringify(this._cache)); - } - } - return false; - } - - list() { - return Object.entries(this._cache).map(([domain, elem]) => { - return { - domain: domain, - time: elem.time - } - }) - } -} - -export default NeighborsCache; \ No newline at end of file diff --git a/frontend/src/router.js b/frontend/src/router.js deleted file mode 100644 index 1602093..0000000 --- a/frontend/src/router.js +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 49d9171..0000000 --- a/frontend/src/scss/_card.scss +++ /dev/null @@ -1,58 +0,0 @@ - -.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 deleted file mode 100644 index 73a0d4e..0000000 --- a/frontend/src/scss/_forms.scss +++ /dev/null @@ -1,71 +0,0 @@ -.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 deleted file mode 100644 index 160f891..0000000 --- a/frontend/src/scss/toolshed.scss +++ /dev/null @@ -1,138 +0,0 @@ -$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/store.js b/frontend/src/store.js deleted file mode 100644 index 6d0dd73..0000000 --- a/frontend/src/store.js +++ /dev/null @@ -1,132 +0,0 @@ -import {createStore} from 'vuex'; -import router from '@/router'; -import FallBackResolver from "@/dns"; -import NeighborsCache from "@/neigbors"; -import {createNullAuth, createSignAuth, createTokenAuth, ServerSet, ServerSetUnion} from "@/federation"; - - -export default createStore({ - state: { - local_loaded: false, - last_load: {}, - user: null, - token: null, - keypair: null, - remember: false, - home_servers: null, - resolver: new FallBackResolver(), - unreachable_neighbors: new NeighborsCache(), - }, - mutations: { - setUser(state, user) { - state.user = user; - if (state.remember) - localStorage.setItem('user', user); - }, - setToken(state, token) { - state.token = token; - if (state.remember) - localStorage.setItem('token', token); - }, - setKey(state, keypair) { - state.keypair = nacl.crypto_sign_keypair_from_seed(nacl.from_hex(keypair)) - if (state.remember) - localStorage.setItem('keypair', nacl.to_hex(state.keypair.signSk).slice(0, 64)) - }, - setRemember(state, remember) { - state.remember = remember; - if (!remember) { - localStorage.removeItem('user'); - localStorage.removeItem('token'); - localStorage.removeItem('keypair'); - } - localStorage.setItem('remember', remember); - }, - setHomeServers(state, home_servers) { - state.home_servers = home_servers; - }, - logout(state) { - state.user = null; - state.token = null; - state.keypair = null; - localStorage.removeItem('user'); - localStorage.removeItem('token'); - localStorage.removeItem('keypair'); - router.push('/login'); - }, - load_local(state) { - if (state.local_loaded) - return; - const remember = localStorage.getItem('remember'); - const user = localStorage.getItem('user'); - const token = localStorage.getItem('token'); - const keypair = localStorage.getItem('keypair'); - if (user && token) { - this.commit('setUser', user); - this.commit('setToken', token); - if (keypair) { - this.commit('setKey', keypair) - } - } - state.cache_loaded = true; - } - }, - actions: { - async login({commit, dispatch, state, getters}, {username, password, remember}) { - commit('setRemember', remember); - const data = await dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors)) - .then(set => set.post(getters.nullAuth, '/auth/token/', {username, password})) - if (data.token && data.key) { - commit('setToken', data.token); - commit('setUser', username); - commit('setKey', data.key); - const s = await dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors)) - commit('setHomeServers', s) - return true; - } else { - return false; - } - }, - async lookupServer({state}, {username}) { - const domain = username.split('@')[1] - const request = '_toolshed-server._tcp.' + domain + '.' - return await state.resolver.query(request, 'SRV').then( - (result) => result.map( - (answer) => answer.target + ':' + answer.port)) - }, - async getHomeServers({state, dispatch, commit}) { - if (state.home_servers) - return state.home_servers - const promise = dispatch('lookupServer', {username: state.user}).then(servers => new ServerSet(servers, state.unreachable_neighbors)) - commit('setHomeServers', promise) - return promise - }, - async getFriendServers({state, dispatch, commit}, {username}) { - return dispatch('lookupServer', {username}).then(servers => new ServerSet(servers, state.unreachable_neighbors)) - }, - }, - getters: { - isLoggedIn(state) { - if (!state.local_loaded) { - state.remember = localStorage.getItem('remember') === 'true' - state.user = localStorage.getItem('user') - state.token = localStorage.getItem('token') - const keypair = localStorage.getItem('keypair') - if (keypair) - state.keypair = nacl.crypto_sign_keypair_from_seed(nacl.from_hex(keypair)) - state.local_loaded = true - } - - return state.user !== null && state.token !== null; - }, - signAuth(state) { - return createSignAuth(state.user, state.keypair.signSk) - }, - tokenAuth(state) { - return createTokenAuth(state.token) - }, - nullAuth(state) { - return createNullAuth({}) - }, - } -}) \ No newline at end of file diff --git a/frontend/src/views/Index.vue b/frontend/src/views/Index.vue deleted file mode 100644 index 90fbeb1..0000000 --- a/frontend/src/views/Index.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue deleted file mode 100644 index 7a2097b..0000000 --- a/frontend/src/views/Login.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue deleted file mode 100644 index f0091d0..0000000 --- a/frontend/src/views/Register.vue +++ /dev/null @@ -1,163 +0,0 @@ - - - - - \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js deleted file mode 100644 index f6a3db5..0000000 --- a/frontend/vite.config.js +++ /dev/null @@ -1,35 +0,0 @@ -import {fileURLToPath, URL} from 'node:url' - -import {defineConfig} from 'vite' -import vue from '@vitejs/plugin-vue' -import * as fs from "fs"; - -export default defineConfig({ - plugins: [vue()], - resolve: { - alias: { - '@': fileURLToPath(new URL('./src', import.meta.url)) - } - }, - server: { - host: true, - cors: true, - headers: { - //allow all origins - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Origin, Content-Type, X-Auth-Token, Authorization, Accept, charset, boundary, Content-Length', - 'Access-Control-Allow-Credentials': 'true', - 'Access-Control-Max-Age': '86400', - 'Content-Security-Policy': 'default-src \'self\';' - + ' script-src \'self\' \'wasm-unsafe-eval\';' - + ' style-src \'self\' \'unsafe-inline\';' - + ' img-src \'self\' data:; ' - + ' connect-src * data:', // TODO: change * to https://* for production (probably in nginx config not here) - }, - }, - test: { - include: ['src/tests/**/*.js'], - globals: true, - environment: "jsdom" - } -}) diff --git a/mkdocs.yml b/mkdocs.yml index 64c248f..dadf43a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,4 +24,3 @@ extra_javascript: - https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.js extra_css: - toolshed.css -site_url: https://localhost:8080/wiki/