diff --git a/.build.yml b/.build.yml index 0061d73..f5f406d 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 --parallel-mode --concurrency=multiprocessing manage.py test --parallel=$(nproc) && coverage report diff --git a/README.md b/README.md index c00c7fb..7f91e3e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@ # toolshed - -## foo - ## Development ``` bash @@ -90,4 +87,4 @@ for detailed instructions see [docs](/docs/deployment.md). ``` bash cli-client/toolshed-client.py --key --user name@example.com --host 1.2.3.4:8000 getinventory -``` +``` \ No newline at end of file diff --git a/backend/.env.dist b/backend/.env.dist index 4cf4790..24a8a42 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -1,4 +1,2 @@ -ALLOWED_HOSTS="localhost,127.0.0.1" - -SERVE_X_ACCEL_REDIRECT=True \ No newline at end of file +ALLOWED_HOSTS="localhost,127.0.0.1" \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 9e12f8f..4581caf 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -6,7 +6,7 @@ RUN pip install --upgrade pip && pip install -r requirements.txt COPY . /app RUN python configure.py RUN python manage.py collectstatic --noinput -CMD python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --insecure +CMD python manage.py runserver 0.0.0.0:8000 --insecure # TODO serve static files with nginx and remove --insecure EXPOSE 8000 diff --git a/backend/authentication/admin.py b/backend/authentication/admin.py index 34131ce..557cf94 100644 --- a/backend/authentication/admin.py +++ b/backend/authentication/admin.py @@ -1,7 +1,6 @@ from django.contrib import admin -from authentication.models import ToolshedUser, KnownIdentity, FriendRequestOutgoing, FriendRequestIncoming, \ - AccountPreference +from authentication.models import ToolshedUser, KnownIdentity, FriendRequestOutgoing, FriendRequestIncoming class ToolshedUserAdmin(admin.ModelAdmin): @@ -9,11 +8,6 @@ class ToolshedUserAdmin(admin.ModelAdmin): search_fields = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'is_active', 'date_joined', 'domain') -class AccountPreferenceAdmin(admin.ModelAdmin): - list_display = ('user', 'key', 'value') - search_fields = ('user__username', 'key') - - class KnownIdentityAdmin(admin.ModelAdmin): list_display = ('username', 'domain', 'public_key') search_fields = ('username', 'domain', 'public_key') @@ -33,4 +27,3 @@ admin.site.register(ToolshedUser, ToolshedUserAdmin) admin.site.register(KnownIdentity, KnownIdentityAdmin) admin.site.register(FriendRequestOutgoing, FriendRequestOutgoingAdmin) admin.site.register(FriendRequestIncoming, FriendRequestIncomingAdmin) -admin.site.register(AccountPreference, AccountPreferenceAdmin) diff --git a/backend/authentication/api.py b/backend/authentication/api.py index f5c4542..eefcdd4 100644 --- a/backend/authentication/api.py +++ b/backend/authentication/api.py @@ -9,57 +9,12 @@ from rest_framework.authtoken.models import Token from rest_framework.authtoken.views import ObtainAuthToken from rest_framework.response import Response -from authentication.models import ToolshedUser, AccountPreference -from authentication.signature_auth import SignatureAuthenticationLocal, SignatureAuthentication, \ - split_userhandle_or_throw -from files.models import File -from files.serializers import FileSerializer +from authentication.models import ToolshedUser +from authentication.signature_auth import SignatureAuthenticationLocal from hostadmin.models import Domain router = routers.SimpleRouter() -# Schema for the account-level preferences a client may store on the server (see -# AccountPreference). Device-level preferences are never sent here - they stay in the -# browser's local storage since they describe the device, not the account. -PREFERENCE_DEFINITIONS = [ - { - 'key': 'ui.compact_mode', - 'type': 'boolean', - 'default': False, - 'label': 'Compact mode', - 'description': 'Show denser item rows and reduce spacing in lists.', - }, - { - 'key': 'ui.default_search_scope', - 'type': 'enum', - 'options': ['inventory', 'friends', 'all'], - 'default': 'inventory', - 'label': 'Default search scope', - 'description': 'Choose where the global search starts.', - }, - { - 'key': 'notifications.desktop_enabled', - 'type': 'boolean', - 'default': True, - 'label': 'Desktop notifications', - 'description': 'Enable in-browser notifications for important updates.', - }, - { - 'key': 'files.max_upload_mb', - 'type': 'integer', - 'default': 25, - 'label': 'Default upload size limit (MB)', - 'description': 'Used as a prefill hint in upload dialogs.', - }, - { - 'key': 'ui.experimental_flags', - 'type': 'json', - 'default': {}, - 'label': 'Experimental flags', - 'description': 'Optional JSON toggles for feature previews.', - }, -] - class UserAuthToken(ObtainAuthToken): @@ -98,70 +53,15 @@ class UserViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated, IsAdminUser] -@api_view(['GET', 'PATCH']) +@api_view(['GET']) @permission_classes([IsAuthenticated]) @authentication_classes([SignatureAuthenticationLocal]) def getUserInfo(request): - """Get or update the authenticated local user's own account info. Only usable by the - account owner on their own home server - see getUserProfile for viewing another (friend) - user's public profile.""" user = request.user - if request.method == 'PATCH': - old_file = user.profile_picture - if 'profile_picture' in request.data: - profile_picture = request.data.get('profile_picture') - if profile_picture is None: - user.profile_picture = None - elif type(profile_picture) == dict: - serializer = FileSerializer(data=profile_picture) - if not serializer.is_valid(): - return Response(serializer.errors, status=400) - user.profile_picture = serializer.save() - else: - return Response({'profile_picture': 'Must be null or an object with data and mime_type.'}, status=400) - elif 'profile_picture_id' in request.data: - profile_picture_id = request.data.get('profile_picture_id') - if profile_picture_id is None: - user.profile_picture = None - else: - try: - user.profile_picture = File.objects.get(id=profile_picture_id) - except File.DoesNotExist: - return Response({'profile_picture_id': 'File does not exist.'}, status=400) - user.save() - - if old_file and old_file != user.profile_picture and old_file.connected_items.count() == 0 and old_file.profile_picture_users.count() == 0: - old_file.delete() - return Response({ 'username': user.username, 'domain': user.domain, - 'email': user.email, - 'profile_picture': FileSerializer(user.profile_picture).data if user.profile_picture else None, - }) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([SignatureAuthentication]) -def getUserProfile(request, handle): - """Get another local user's public profile by handle (username@domain), e.g. so a friend - can look up someone's avatar. The caller must be a friend of that user (or the user - itself, signing with their own known identity rather than their local credentials).""" - try: - username, domain = split_userhandle_or_throw(handle) - except ValueError: - return Response(status=400) - try: - target = ToolshedUser.objects.get(username=username, domain=domain) - except ToolshedUser.DoesNotExist: - return Response(status=404) - if target not in request.user.friends_or_self(): - return Response(status=403) - return Response({ - 'username': target.username, - 'domain': target.domain, - 'profile_picture': FileSerializer(target.profile_picture).data if target.profile_picture else None, + 'email': user.email }) @@ -199,51 +99,11 @@ def registerUser(request): return Response({'errors': {'domain': 'Domain does not exist or is not open for registration'}}, status=400) -@api_view(['GET']) -@permission_classes([]) -@authentication_classes([]) -def preference_definitions(request): - """Return the schema (types, defaults, labels) for the account preferences clients may set.""" - return Response(PREFERENCE_DEFINITIONS) - - -@api_view(['GET', 'PUT']) -@permission_classes([IsAuthenticated]) -@authentication_classes([SignatureAuthenticationLocal]) -def account_preferences(request): - """Get or bulk-upsert the authenticated user's account-level preferences. - - GET returns the current preferences as a {key: value} dict. PUT accepts a {key: value} - dict of one or more preferences to set/overwrite; unspecified keys are left untouched. - """ - if request.method == 'PUT': - if not isinstance(request.data, dict): - return Response({'detail': 'Expected an object of key/value pairs.'}, status=400) - for key, value in request.data.items(): - AccountPreference.objects.update_or_create(user=request.user, key=key, defaults={'value': value}) - - preferences = {pref.key: pref.value for pref in request.user.preferences.all()} - return Response(preferences) - - -@api_view(['DELETE']) -@permission_classes([IsAuthenticated]) -@authentication_classes([SignatureAuthenticationLocal]) -def account_preference_detail(request, key): - """Reset a single account-level preference back to its default by deleting it.""" - AccountPreference.objects.filter(user=request.user, key=key).delete() - return Response(status=204) - - router.register(r'users', UserViewSet) urlpatterns = [ path('', include(router.urls)), path('user/', getUserInfo), - path('user//', getUserProfile), path('register/', registerUser), path('token/', UserAuthToken.as_view()), - path('preferences/', preference_definitions), - path('self/preferences/', account_preferences), - path('self/preferences//', account_preference_detail), ] diff --git a/backend/authentication/migrations/0002_toolsheduser_profile_picture.py b/backend/authentication/migrations/0002_toolsheduser_profile_picture.py deleted file mode 100644 index 8078aac..0000000 --- a/backend/authentication/migrations/0002_toolsheduser_profile_picture.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('files', '0001_initial'), - ('authentication', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='toolsheduser', - name='profile_picture', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='profile_picture_users', to='files.file'), - ), - ] - diff --git a/backend/authentication/migrations/0003_accountpreference.py b/backend/authentication/migrations/0003_accountpreference.py deleted file mode 100644 index 712c593..0000000 --- a/backend/authentication/migrations/0003_accountpreference.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('authentication', '0002_toolsheduser_profile_picture'), - ] - - operations = [ - migrations.CreateModel( - name='AccountPreference', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.CharField(max_length=255)), - ('value', models.JSONField()), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='preferences', - to='authentication.toolsheduser')), - ], - options={ - 'unique_together': {('user', 'key')}, - }, - ), - ] - diff --git a/backend/authentication/models.py b/backend/authentication/models.py index c6044a6..5ef3890 100644 --- a/backend/authentication/models.py +++ b/backend/authentication/models.py @@ -86,8 +86,6 @@ class ToolshedUser(AbstractUser): domain = models.CharField(max_length=255, default='localhost') private_key = models.CharField(max_length=255) public_identity = models.ForeignKey(KnownIdentity, on_delete=models.CASCADE, related_name='user') - profile_picture = models.ForeignKey('files.File', on_delete=models.SET_NULL, null=True, blank=True, - related_name='profile_picture_users') objects = ToolshedUserManager() class Meta: @@ -113,23 +111,6 @@ class ToolshedUser(AbstractUser): return self.public_identity.public_key -class AccountPreference(models.Model): - """A single account-level (server-synced, cross-device) user preference, stored as a key/value pair. - - Device-level preferences are intentionally *not* stored here - they stay in the browser's - local storage since they describe the device, not the account. - """ - user = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='preferences') - key = models.CharField(max_length=255) - value = models.JSONField() - - class Meta: - unique_together = ('user', 'key') - - def __str__(self): - return f"{self.user}: {self.key}" - - class FriendRequestOutgoing(models.Model): secret = models.CharField(max_length=255) befriender_user = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='friend_requests_outgoing') diff --git a/backend/authentication/signature_auth.py b/backend/authentication/signature_auth.py index 251ac04..f6b280a 100644 --- a/backend/authentication/signature_auth.py +++ b/backend/authentication/signature_auth.py @@ -106,19 +106,11 @@ def authenticate_request_against_local_users(request, raw_request_body): class SignatureAuthentication(authentication.BaseAuthentication): def authenticate(self, request): - identity = authenticate_request_against_known_identities(request, request.body.decode('utf-8')) - # Returning a bare None (rather than a (None, None) tuple) tells DRF this - # authenticator doesn't apply, so it moves on to the next authenticator in the - # authentication_classes list instead of treating the request as authenticated - # with an empty user. - if identity is None: - return None - return identity, None + return authenticate_request_against_known_identities( + request, request.body.decode('utf-8')), None class SignatureAuthenticationLocal(authentication.BaseAuthentication): def authenticate(self, request): - user = authenticate_request_against_local_users(request, request.body.decode('utf-8')) - if user is None: - return None - return user, None + return authenticate_request_against_local_users( + request, request.body.decode('utf-8')), None diff --git a/backend/authentication/tests/test_auth.py b/backend/authentication/tests/test_auth.py index 460095b..0a3caf1 100644 --- a/backend/authentication/tests/test_auth.py +++ b/backend/authentication/tests/test_auth.py @@ -1,5 +1,4 @@ import json -import base64 from django.test import Client, RequestFactory from nacl.encoding import HexEncoder @@ -7,7 +6,6 @@ from nacl.signing import SigningKey from authentication.models import ToolshedUser, KnownIdentity from authentication.tests import UserTestMixin, SignatureAuthClient, DummyExternalUser, ToolshedTestCase -from files.models import File class AuthorizationTestCase(ToolshedTestCase): @@ -242,7 +240,6 @@ class UserApiTestCase(UserTestMixin, ToolshedTestCase): self.assertEqual(reply.json()['username'], 'testuser1') self.assertEqual(reply.json()['domain'], 'example.com') self.assertEqual(reply.json()['email'], 'test1@abc.de') - self.assertIsNone(reply.json()['profile_picture']) def test_user_info2(self): target = "/auth/user/" @@ -252,50 +249,6 @@ class UserApiTestCase(UserTestMixin, ToolshedTestCase): self.assertEqual(reply.status_code, 200) self.assertEqual(reply.json()['username'], 'testuser1') self.assertEqual(reply.json()['domain'], 'example.com') - self.assertIsNone(reply.json()['profile_picture']) - - def test_user_info_patch_profile_picture(self): - content = base64.b64encode(b'user-profile-image').decode('utf-8') - reply = self.client.patch('/auth/user/', self.f['local_user1'], { - 'profile_picture': { - 'data': content, - 'mime_type': 'image/png' - } - }) - self.assertEqual(reply.status_code, 200) - self.assertTrue(reply.json()['profile_picture']) - self.assertEqual(reply.json()['profile_picture']['mime_type'], 'image/png') - self.assertEqual(File.objects.count(), 1) - self.f['local_user1'].refresh_from_db() - self.assertIsNotNone(self.f['local_user1'].profile_picture) - - def test_user_info_patch_profile_picture_clear(self): - encoded_content = base64.b64encode(b'user-profile-image').decode('utf-8') - test_file = File.objects.create(mime_type='image/png', data=encoded_content) - self.f['local_user1'].profile_picture = test_file - self.f['local_user1'].save() - - reply = self.client.patch('/auth/user/', self.f['local_user1'], {'profile_picture': None}) - self.assertEqual(reply.status_code, 200) - self.assertIsNone(reply.json()['profile_picture']) - self.f['local_user1'].refresh_from_db() - self.assertIsNone(self.f['local_user1'].profile_picture) - self.assertFalse(File.objects.filter(id=test_file.id).exists()) - - def test_user_info_patch_profile_picture_invalid(self): - reply = self.client.patch('/auth/user/', self.f['local_user1'], {'profile_picture': 'invalid'}) - self.assertEqual(reply.status_code, 400) - - def test_user_info_patch_profile_picture_id(self): - encoded_content = base64.b64encode(b'user-profile-image-by-id').decode('utf-8') - test_file = File.objects.create(mime_type='image/jpeg', data=encoded_content) - reply = self.client.patch('/auth/user/', self.f['local_user1'], {'profile_picture_id': test_file.id}) - self.assertEqual(reply.status_code, 200) - self.assertEqual(reply.json()['profile_picture']['id'], test_file.id) - - def test_user_info_patch_profile_picture_id_not_found(self): - reply = self.client.patch('/auth/user/', self.f['local_user1'], {'profile_picture_id': 999999}) - self.assertEqual(reply.status_code, 400) def test_user_info_fail(self): reply = self.anonymous_client.get('/auth/user/') @@ -355,52 +308,6 @@ class UserApiTestCase(UserTestMixin, ToolshedTestCase): self.assertEqual(reply.status_code, 403) -class UserProfileByHandleApiTestCase(UserTestMixin, ToolshedTestCase): - """Tests for GET /auth/user// - viewing another (friend) user's public profile.""" - - def setUp(self): - super().setUp() - self.prepare_users() - self.f['local_user1'].friends.add(self.f['ext_user1'].public_identity) - self.anonymous_client = Client(SERVER_NAME='testserver') - self.client = SignatureAuthClient() - - def test_view_friend_profile(self): - target = '/auth/user/' + str(self.f['local_user1']) + '/' - reply = self.client.get(target, self.f['ext_user1']) - self.assertEqual(reply.status_code, 200) - self.assertEqual(reply.json()['username'], 'testuser1') - self.assertEqual(reply.json()['domain'], 'example.com') - self.assertIsNone(reply.json()['profile_picture']) - self.assertNotIn('email', reply.json()) - - def test_view_own_profile_via_handle(self): - target = '/auth/user/' + str(self.f['local_user1']) + '/' - reply = self.client.get(target, self.f['local_user1']) - self.assertEqual(reply.status_code, 200) - self.assertEqual(reply.json()['username'], 'testuser1') - - def test_view_profile_not_friend(self): - target = '/auth/user/' + str(self.f['local_user1']) + '/' - reply = self.client.get(target, self.f['ext_user2']) - self.assertEqual(reply.status_code, 403) - - def test_view_profile_unknown_user(self): - target = '/auth/user/nosuchuser@example.com/' - reply = self.client.get(target, self.f['ext_user1']) - self.assertEqual(reply.status_code, 404) - - def test_view_profile_bad_handle(self): - target = '/auth/user/notahandle/' - reply = self.client.get(target, self.f['ext_user1']) - self.assertEqual(reply.status_code, 400) - - def test_view_profile_unauthenticated(self): - target = '/auth/user/' + str(self.f['local_user1']) + '/' - reply = self.anonymous_client.get(target) - self.assertEqual(reply.status_code, 403) - - class FriendApiTestCase(UserTestMixin, ToolshedTestCase): def setUp(self): super().setUp() diff --git a/backend/backend/settings.py b/backend/backend/settings.py index 1d09378..82f2e67 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -86,8 +86,6 @@ CORS_ALLOW_ALL_ORIGINS = True USE_X_FORWARDED_HOST = True SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') -SERVE_X_ACCEL_REDIRECT = os.environ.get('SERVE_X_ACCEL_REDIRECT', 'False').lower() == 'true' - ROOT_URLCONF = 'backend.urls' TEMPLATES = [ diff --git a/backend/backend/urls.py b/backend/backend/urls.py index 709224d..ab9c3a2 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -38,7 +38,6 @@ urlpatterns = [ path('api/', include('toolshed.api.inventory')), path('api/', include('toolshed.api.info')), path('api/', include('toolshed.api.files')), - path('api/', include('toolshed.api.offlinedata')), path('media/', include('files.media_urls')), path('docs/', schema_view.with_ui('swagger', cache_timeout=0), name='api-docs'), ] diff --git a/backend/files/media_urls.py b/backend/files/media_urls.py index 92566ed..aca590d 100644 --- a/backend/files/media_urls.py +++ b/backend/files/media_urls.py @@ -1,7 +1,5 @@ from django.http import HttpResponse from django.urls import path -from django.db.models import Q -from django.conf import settings from drf_yasg.utils import swagger_auto_schema from rest_framework import status from rest_framework.decorators import api_view, permission_classes, authentication_classes @@ -17,30 +15,16 @@ from files.models import File @permission_classes([IsAuthenticated]) @authentication_classes([SignatureAuthentication]) def media_urls(request, hash_path): - # Note: CORS headers are NOT set here - django-cors-headers (CorsMiddleware, - # configured in settings.py) adds them to every Django response automatically, so - # setting them manually on these responses would just be redundant. The one exception - # is the SERVE_X_ACCEL_REDIRECT path: nginx replaces this response entirely when it - # follows the X-Accel-Redirect and serves the file itself, so the CORS header for that - # case has to be configured in nginx's `location /redirect_media/` block instead. try: - file = File.objects.filter( - Q(connected_items__owner__in=request.user.friends_or_self()) | - Q(profile_picture_users__in=request.user.friends_or_self()) - ).distinct().get( + file = File.objects.filter(connected_items__owner__in=request.user.friends_or_self()).distinct().get( file=hash_path) - if settings.SERVE_X_ACCEL_REDIRECT: - return HttpResponse(status=status.HTTP_200_OK, - content_type=file.mime_type, - headers={ - 'X-Accel-Redirect': f'/redirect_media/{hash_path}', - }) # TODO Expires and Cache-Control - else: - return HttpResponse(status=status.HTTP_200_OK, - content_type=file.mime_type, - content=open(file.file.path, 'rb').read()) - + return HttpResponse(status=status.HTTP_200_OK, + content_type=file.mime_type, + headers={ + 'X-Accel-Redirect': f'/redirect_media/{hash_path}', + 'Access-Control-Allow-Origin': '*', + }) # TODO Expires and Cache-Control except File.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) diff --git a/backend/files/tests.py b/backend/files/tests.py index 2d68e88..40f6413 100644 --- a/backend/files/tests.py +++ b/backend/files/tests.py @@ -1,7 +1,7 @@ from django.core.files.base import ContentFile from django.core.files.storage import DefaultStorage from django.db import IntegrityError, transaction -from django.test import Client, override_settings +from django.test import Client from authentication.tests import SignatureAuthClient, ToolshedTestCase, UserTestMixin from toolshed.tests import InventoryTestMixin from nacl.hash import sha256 @@ -120,28 +120,28 @@ class MediaUrlTestCase(FilesTestMixin, UserTestMixin, InventoryTestMixin, Toolsh self.f['item2'].files.add(self.f['test_file1']) -# def test_file_url(self): -# reply = client.get( -# f"/media/{self.f['hash1'][:2]}/{self.f['hash1'][2:4]}/{self.f['hash1'][4:6]}/{self.f['hash1'][6:]}", -# self.f['local_user1']) -# self.assertEqual(reply.status_code, 200) -# self.assertEqual(reply.headers['X-Accel-Redirect'], -# f"/redirect_media/{self.f['hash1'][:2]}/{self.f['hash1'][2:4]}/{self.f['hash1'][4:6]}/{self.f['hash1'][6:]}") -# self.assertEqual(reply.headers['Content-Type'], self.f['test_file1'].mime_type) -# reply = client.get( -# f"/media/{self.f['hash2'][:2]}/{self.f['hash2'][2:4]}/{self.f['hash2'][4:6]}/{self.f['hash2'][6:]}", -# self.f['local_user1']) -# self.assertEqual(reply.status_code, 200) -# self.assertEqual(reply.headers['X-Accel-Redirect'], -# f"/redirect_media/{self.f['hash2'][:2]}/{self.f['hash2'][2:4]}/{self.f['hash2'][4:6]}/{self.f['hash2'][6:]}") -# self.assertEqual(reply.headers['Content-Type'], self.f['test_file2'].mime_type) -# reply = client.get( -# f"/media/{self.f['hash2'][:2]}/{self.f['hash2'][2:4]}/{self.f['hash2'][4:6]}/{self.f['hash2'][6:]}", -# self.f['local_user2']) -# self.assertEqual(reply.status_code, 200) -# self.assertEqual(reply.headers['X-Accel-Redirect'], -# f"/redirect_media/{self.f['hash2'][:2]}/{self.f['hash2'][2:4]}/{self.f['hash2'][4:6]}/{self.f['hash2'][6:]}") -# self.assertEqual(reply.headers['Content-Type'], self.f['test_file2'].mime_type) + def test_file_url(self): + reply = client.get( + f"/media/{self.f['hash1'][:2]}/{self.f['hash1'][2:4]}/{self.f['hash1'][4:6]}/{self.f['hash1'][6:]}", + self.f['local_user1']) + self.assertEqual(reply.status_code, 200) + self.assertEqual(reply.headers['X-Accel-Redirect'], + f"/redirect_media/{self.f['hash1'][:2]}/{self.f['hash1'][2:4]}/{self.f['hash1'][4:6]}/{self.f['hash1'][6:]}") + self.assertEqual(reply.headers['Content-Type'], self.f['test_file1'].mime_type) + reply = client.get( + f"/media/{self.f['hash2'][:2]}/{self.f['hash2'][2:4]}/{self.f['hash2'][4:6]}/{self.f['hash2'][6:]}", + self.f['local_user1']) + self.assertEqual(reply.status_code, 200) + self.assertEqual(reply.headers['X-Accel-Redirect'], + f"/redirect_media/{self.f['hash2'][:2]}/{self.f['hash2'][2:4]}/{self.f['hash2'][4:6]}/{self.f['hash2'][6:]}") + self.assertEqual(reply.headers['Content-Type'], self.f['test_file2'].mime_type) + reply = client.get( + f"/media/{self.f['hash2'][:2]}/{self.f['hash2'][2:4]}/{self.f['hash2'][4:6]}/{self.f['hash2'][6:]}", + self.f['local_user2']) + self.assertEqual(reply.status_code, 200) + self.assertEqual(reply.headers['X-Accel-Redirect'], + f"/redirect_media/{self.f['hash2'][:2]}/{self.f['hash2'][2:4]}/{self.f['hash2'][4:6]}/{self.f['hash2'][6:]}") + self.assertEqual(reply.headers['Content-Type'], self.f['test_file2'].mime_type) def test_file_url_fail(self): reply = client.get('/media/{}/'.format('nonexistent'), self.f['local_user1']) @@ -165,33 +165,3 @@ class MediaUrlTestCase(FilesTestMixin, UserTestMixin, InventoryTestMixin, Toolsh self.f['ext_user1']) self.assertEqual(reply.status_code, 404) self.assertTrue('X-Accel-Redirect' not in reply.headers) - - @override_settings(SERVE_X_ACCEL_REDIRECT=True) - def test_profile_picture_url(self): - self.f['local_user1'].profile_picture = self.f['test_file3'] - self.f['local_user1'].save() - - reply = client.get( - f"/media/{self.f['hash3'][:2]}/{self.f['hash3'][2:4]}/{self.f['hash3'][4:6]}/{self.f['hash3'][6:]}", - self.f['local_user1']) - self.assertEqual(reply.status_code, 200) - - @override_settings(SERVE_X_ACCEL_REDIRECT=True) - def test_profile_picture_url_friend(self): - self.f['local_user1'].profile_picture = self.f['test_file3'] - self.f['local_user1'].save() - - reply = client.get( - f"/media/{self.f['hash3'][:2]}/{self.f['hash3'][2:4]}/{self.f['hash3'][4:6]}/{self.f['hash3'][6:]}", - self.f['local_user2']) - self.assertEqual(reply.status_code, 200) - - def test_profile_picture_url_not_friend(self): - self.f['local_user1'].profile_picture = self.f['test_file3'] - self.f['local_user1'].save() - - reply = client.get( - f"/media/{self.f['hash3'][:2]}/{self.f['hash3'][2:4]}/{self.f['hash3'][4:6]}/{self.f['hash3'][6:]}", - self.f['ext_user1']) - self.assertEqual(reply.status_code, 404) - diff --git a/backend/hostadmin/serializers.py b/backend/hostadmin/serializers.py index 03c1805..02fadb6 100644 --- a/backend/hostadmin/serializers.py +++ b/backend/hostadmin/serializers.py @@ -41,10 +41,6 @@ class DomainSerializer(serializers.ModelSerializer): class CategorySerializer(serializers.ModelSerializer): parent = SlugPathField(slug_field='name', queryset=Category.objects.all(), required=False) - handle = serializers.SerializerMethodField() - - def get_handle(self, obj): - return obj.get_handle() def validate(self, attrs): if 'name' in attrs: @@ -60,17 +56,13 @@ class CategorySerializer(serializers.ModelSerializer): class Meta: model = Category - fields = ['name', 'description', 'parent', 'origin', 'handle'] - read_only_fields = ['origin', 'handle'] + fields = ['name', 'description', 'parent', 'origin'] + read_only_fields = ['origin'] ref_name = 'HostAdminCategory' class PropertySerializer(serializers.ModelSerializer): category = SlugPathField(slug_field='name', queryset=Category.objects.all(), required=False) - handle = serializers.SerializerMethodField() - - def get_handle(self, obj): - return obj.get_handle() def validate(self, attrs): if 'name' in attrs: @@ -87,17 +79,13 @@ class PropertySerializer(serializers.ModelSerializer): class Meta: model = Property fields = ['name', 'description', 'category', 'unit_symbol', 'unit_name', 'unit_name_plural', 'base2_prefix', - 'dimensions', 'origin', 'handle'] - read_only_fields = ['origin', 'handle'] + 'dimensions', 'origin'] + read_only_fields = ['origin'] ref_name = 'HostAdminProperty' class TagSerializer(serializers.ModelSerializer): category = SlugPathField(slug_field='name', queryset=Category.objects.all(), required=False) - handle = serializers.SerializerMethodField() - - def get_handle(self, obj): - return obj.get_handle() def validate(self, attrs): if 'name' in attrs: @@ -113,6 +101,6 @@ class TagSerializer(serializers.ModelSerializer): class Meta: model = Tag - fields = ['name', 'description', 'category', 'origin', 'handle'] - read_only_fields = ['origin', 'handle'] + fields = ['name', 'description', 'category', 'origin'] + read_only_fields = ['origin'] ref_name = 'HostAdminTag' diff --git a/backend/shared_data/base.json b/backend/shared_data/base.json deleted file mode 100644 index d961bb8..0000000 --- a/backend/shared_data/base.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "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/ee.json b/backend/shared_data/ee.json deleted file mode 100644 index 3b0e73e..0000000 --- a/backend/shared_data/ee.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "depends": [ "git:base" ], - "categories": [ - { "name": "electronics", "parent": "hardware"}, - { "name": "electronics", "parent": "tools"}, - { "name": "bus", "parent": "hardware/electronics"}, - { "name": "mcu", "parent": "hardware/electronics"}, - { "name": "wireless", "parent": "hardware/electronics"} - ], - "tags": [ - {"name": "smt", "category": "hardware/electronics"}, - {"name": "tht", "category": "hardware/electronics"}, - {"name": "adapter", "category": "hardware/electronics"}, - {"name": "amperemeter", "category": "tools/electronics"}, - {"name": "cable", "category": "hardware/electronics"}, - {"name": "camera", "category": "tools/electronics"}, - {"name": "connector", "category": "hardware/electronics"}, - {"name": "flux", "category": "hardware/electronics"}, - {"name": "microscope", "category": "tools/electronics"}, - {"name": "multimeter", "category": "tools/electronics"}, - {"name": "oscilloscope", "category": "tools/electronics"}, - {"name": "power supply", "category": "hardware/electronics"}, - {"name": "solder", "category": "hardware/electronics"}, - {"name": "soldering", "category": "tools/electronics"}, - {"name": "voltmeter", "category": "tools/electronics"}, - {"name": "charger", "category": "hardware/electronics"}, - {"name": "actuator", "category": "hardware/electronics"}, - {"name": "battery", "category": "hardware/electronics"}, - {"name": "capacitor", "category": "hardware/electronics"}, - {"name": "diode", "category": "hardware/electronics"}, - {"name": "display", "category": "hardware/electronics"}, - {"name": "encoder", "category": "hardware/electronics"}, - {"name": "fuse", "category": "hardware/electronics"}, - {"name": "inductor", "category": "hardware/electronics"}, - {"name": "inverter", "category": "hardware/electronics"}, - {"name": "lcd", "category": "hardware/electronics"}, - {"name": "led", "category": "hardware/electronics"}, - {"name": "motor", "category": "hardware/electronics"}, - {"name": "oscillator", "category": "hardware/electronics"}, - {"name": "potentiometer", "category": "hardware/electronics"}, - {"name": "relay", "category": "hardware/electronics"}, - {"name": "resistor", "category": "hardware/electronics"}, - {"name": "sensor", "category": "hardware/electronics"}, - {"name": "servo", "category": "hardware/electronics"}, - {"name": "stepper", "category": "hardware/electronics"}, - {"name": "switch", "category": "hardware/electronics"}, - {"name": "thermistor", "category": "hardware/electronics"}, - {"name": "thermocouple", "category": "hardware/electronics"}, - {"name": "transformer", "category": "hardware/electronics"}, - {"name": "transistor", "category": "hardware/electronics"}, - {"name": "bluetooth", "category": "hardware/electronics/wireless"}, - {"name": "gps", "category": "hardware/electronics/wireless"}, - {"name": "gsm", "category": "hardware/electronics/wireless"}, - {"name": "lora", "category": "hardware/electronics/wireless"}, - {"name": "nfc", "category": "hardware/electronics/wireless"}, - {"name": "rfid", "category": "hardware/electronics/wireless"}, - {"name": "thread", "category": "hardware/electronics/wireless"}, - {"name": "wifi", "category": "hardware/electronics/wireless"}, - {"name": "zigbee", "category": "hardware/electronics/wireless"}, - {"name": "zwave", "category": "hardware/electronics/wireless"}, - {"name": "can", "category": "hardware/electronics/bus"}, - {"name": "ethernet", "category": "hardware/electronics/bus"}, - {"name": "i2c", "category": "hardware/electronics/bus"}, - {"name": "lin", "category": "hardware/electronics/bus"}, - {"name": "spi", "category": "hardware/electronics/bus"}, - {"name": "uart", "category": "hardware/electronics/bus"}, - {"name": "usb", "category": "hardware/electronics/bus"}, - {"name": "arduino", "category": "hardware/electronics/mcu"}, - {"name": "atmega", "category": "hardware/electronics/mcu"}, - {"name": "attiny", "category": "hardware/electronics/mcu"}, - {"name": "beaglebone", "category": "hardware/electronics/mcu"}, - {"name": "esp32", "category": "hardware/electronics/mcu"}, - {"name": "esp8266", "category": "hardware/electronics/mcu"}, - {"name": "nucleo", "category": "hardware/electronics/mcu"}, - {"name": "raspberry", "category": "hardware/electronics/mcu"}, - {"name": "stm32", "category": "hardware/electronics/mcu"}, - {"name": "stm8", "category": "hardware/electronics/mcu"}, - {"name": "6502", "category": "hardware/electronics/mcu"}, - {"name": "8051", "category": "hardware/electronics/mcu"}, - {"name": "arm", "category": "hardware/electronics/mcu"}, - {"name": "avr", "category": "hardware/electronics/mcu"}, - {"name": "cortex", "category": "hardware/electronics/mcu"}, - {"name": "m68k", "category": "hardware/electronics/mcu"}, - {"name": "mips", "category": "hardware/electronics/mcu"}, - {"name": "pic", "category": "hardware/electronics/mcu"}, - {"name": "powerpc", "category": "hardware/electronics/mcu"}, - {"name": "risc-v", "category": "hardware/electronics/mcu"}, - {"name": "x86", "category": "hardware/electronics/mcu"}, - {"name": "xtensa", "category": "hardware/electronics/mcu"}, - {"name": "z80", "category": "hardware/electronics/mcu"} - ] -} \ No newline at end of file diff --git a/backend/shared_data/ee_packages.json b/backend/shared_data/ee_packages.json deleted file mode 100644 index 5701202..0000000 --- a/backend/shared_data/ee_packages.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "depends": [ "git:ee" ], - "categories": [ - { "name": "packages", "parent": "hardware/electronics", "description": "Component Packages"} - ], - "tags": [ - {"name": "0201", "category": "hardware/electronics/packages"}, - {"name": "0402", "category": "hardware/electronics/packages"}, - {"name": "0603", "category": "hardware/electronics/packages"}, - {"name": "0805", "category": "hardware/electronics/packages"}, - {"name": "1206", "category": "hardware/electronics/packages"}, - {"name": "1210", "category": "hardware/electronics/packages"}, - {"name": "1812", "category": "hardware/electronics/packages"}, - {"name": "MELF", "category": "hardware/electronics/packages"}, - {"name": "MiniMELF", "category": "hardware/electronics/packages"}, - {"name": "MicroMELF", "category": "hardware/electronics/packages"}, - {"name": "SMC", "category": "hardware/electronics/packages"}, - {"name": "SMB", "category": "hardware/electronics/packages"}, - {"name": "SMA", "category": "hardware/electronics/packages"}, - {"name": "GF1", "category": "hardware/electronics/packages"}, - {"name": "DIP-x", "category": "hardware/electronics/packages", - "description": "Dual Inline Package, needs pin count property to be unambiguous"}, - {"name": "SOD", "category": "hardware/electronics/packages"}, - {"name": "SOD-123", "category": "hardware/electronics/packages"}, - {"name": "SOD-323", "category": "hardware/electronics/packages"}, - {"name": "SOD-523", "category": "hardware/electronics/packages"}, - {"name": "SOD-923", "category": "hardware/electronics/packages"}, - {"name": "SOT", "category": "hardware/electronics/packages"}, - {"name": "SOT23", "category": "hardware/electronics/packages"}, - {"name": "SOT23-3", "category": "hardware/electronics/packages"}, - {"name": "SOT323", "category": "hardware/electronics/packages"}, - {"name": "SOT416", "category": "hardware/electronics/packages"}, - {"name": "SOT23-5", "category": "hardware/electronics/packages"}, - {"name": "SOT353", "category": "hardware/electronics/packages"}, - {"name": "SOT553", "category": "hardware/electronics/packages"}, - {"name": "SOT23-6", "category": "hardware/electronics/packages"}, - {"name": "SOT363", "category": "hardware/electronics/packages"}, - {"name": "SOT563", "category": "hardware/electronics/packages"}, - {"name": "SOT23-8", "category": "hardware/electronics/packages"}, - {"name": "SOT54", "category": "hardware/electronics/packages", "alias":"TO-92"}, - {"name": "SOT143", "category": "hardware/electronics/packages"}, - {"name": "SOT343", "category": "hardware/electronics/packages"}, - {"name": "SOT490", "category": "hardware/electronics/packages"}, - {"name": "SOT89-3", "category": "hardware/electronics/packages"}, - {"name": "SOT89-5", "category": "hardware/electronics/packages"}, - {"name": "SOT223-4", "category": "hardware/electronics/packages"}, - {"name": "SOT223-5", "category": "hardware/electronics/packages"}, - {"name": "SOT223-8", "category": "hardware/electronics/packages"}, - {"name": "TO-3", "category": "hardware/electronics/packages"}, - {"name": "TO-5", "category": "hardware/electronics/packages"}, - {"name": "TO-8", "category": "hardware/electronics/packages"}, - {"name": "TO-18", "category": "hardware/electronics/packages"}, - {"name": "TO-39", "category": "hardware/electronics/packages"}, - {"name": "TO-66", "category": "hardware/electronics/packages"}, - {"name": "TO-92", "category": "hardware/electronics/packages"}, - {"name": "TO-220", "category": "hardware/electronics/packages"}, - {"name": "TO-247", "category": "hardware/electronics/packages"}, - {"name": "TO-251", "category": "hardware/electronics/packages"}, - {"name": "TO-252", "category": "hardware/electronics/packages"}, - {"name": "TO-263", "category": "hardware/electronics/packages"}, - {"name": "TO-264", "category": "hardware/electronics/packages"}, - {"name": "TO-268", "category": "hardware/electronics/packages"}, - {"name": "TO-269", "category": "hardware/electronics/packages"}, - {"name": "SOIC-x", "category": "hardware/electronics/packages", "alias": "SO-x", - "description": "Small Outline Integrated Circuit, needs pin count property to be unambiguous"}, - {"name": "SOJ-x", "category": "hardware/electronics/packages", - "description": "Small Outline J-leaded, needs pin count property to be unambiguous"}, - {"name": "MSOP-x", "category": "hardware/electronics/packages", - "description": "Mini Small Outline Package, needs pin count property to be unambiguous"}, - {"name": "SSOP-x", "category": "hardware/electronics/packages", - "description": "Shrink Small Outline Package, needs pin count property to be unambiguous"}, - {"name": "SOP-x", "category": "hardware/electronics/packages", - "description": "Small Outline Package, needs pin count property to be unambiguous"}, - {"name": "TSOP-x", "category": "hardware/electronics/packages", - "description": "Thin Small Outline Package, needs pin count property to be unambiguous"}, - {"name": "TSSOP-x", "category": "hardware/electronics/packages", - "description": "Thin Shrink Small Outline Package, needs pin count property to be unambiguous"}, - {"name": "QFP-x", "category": "hardware/electronics/packages", - "description": "Quad Flat Package, needs pin count property to be unambiguous"}, - {"name": "TQFP-x", "category": "hardware/electronics/packages", - "description": "Thin Quad Flat Package, needs pin count property to be unambiguous"}, - {"name": "LQFP-x", "category": "hardware/electronics/packages", - "description": "Low-profile Quad Flat Package, needs pin count property to be unambiguous"}, - {"name": "DFN-x", "category": "hardware/electronics/packages", - "description": "Dual Flat No-leaded, needs pin count property to be unambiguous"}, - {"name": "QFN-x", "category": "hardware/electronics/packages", - "description": "Quad Flat No-leaded, needs pin count property to be unambiguous"}, - {"name": "TQFN-x", "category": "hardware/electronics/packages", - "description": "Thin Quad Flat No-leaded, needs pin count property to be unambiguous"}, - {"name": "LQFN-x", "category": "hardware/electronics/packages", - "description": "Low-profile Quad Flat No-leaded, needs pin count property to be unambiguous"}, - {"name": "UQFN-x", "category": "hardware/electronics/packages", - "description": "Ultra-thin Quad Flat No-leaded, needs pin count property to be unambiguous"} - ], - "properties": [ - { "name": "pin count", "unit_symbol": "", "unit_name": "", "unit_name_plural": "" } - ], - "url": "https://en.wikipedia.org/wiki/List_of_integrated_circuit_packaging_types" -} \ No newline at end of file diff --git a/backend/shared_data/electrical.json b/backend/shared_data/electrical.json deleted file mode 100644 index c0a6fac..0000000 --- a/backend/shared_data/electrical.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "depends": ["git:base"], - "categories": [ - { "name": "electrical"}, - { "name": "connectors"}, - { "name": "power", "parent": "connectors" } - ], - "tags": [ - { "name": "braker", "category": "electrical" }, - { "name": "cable", "category": "electrical" }, - { "name": "connector", "category": "electrical" }, - { "name": "plug", "category": "connectors" }, - { "name": "socket", "category": "connectors" }, - { "name": "power", "category": "connectors" }, - { "name": "C1", "category": "connectors/power" }, - { "name": "C2", "category": "connectors/power" }, - { "name": "C3", "category": "connectors/power" }, - { "name": "C4", "category": "connectors/power" }, - { "name": "C5", "category": "connectors/power" }, - { "name": "C6", "category": "connectors/power" }, - { "name": "C7", "category": "connectors/power" }, - { "name": "C7P", "category": "connectors/power" }, - { "name": "C8", "category": "connectors/power" }, - { "name": "C8P", "category": "connectors/power" }, - { "name": "C9", "category": "connectors/power" }, - { "name": "C10", "category": "connectors/power" }, - { "name": "C11", "category": "connectors/power" }, - { "name": "C12", "category": "connectors/power" }, - { "name": "C13", "category": "connectors/power" }, - { "name": "C14", "category": "connectors/power" }, - { "name": "C15", "category": "connectors/power" }, - { "name": "C15A", "category": "connectors/power" }, - { "name": "C16", "category": "connectors/power" }, - { "name": "C16A", "category": "connectors/power" }, - { "name": "C17", "category": "connectors/power" }, - { "name": "C18", "category": "connectors/power" }, - { "name": "C19", "category": "connectors/power" }, - { "name": "C20", "category": "connectors/power" }, - { "name": "C21", "category": "connectors/power" }, - { "name": "C22", "category": "connectors/power" }, - { "name": "C23", "category": "connectors/power" }, - { "name": "C24", "category": "connectors/power" }, - { "name": "Type A", "category": "connectors/power", "description": "NEMA 1-15, U.S. 2 pin" }, - { "name": "Type B", "category": "connectors/power", "description": "NEMA 5-15, U.S. 3 pin" }, - { "name": "Type C", "category": "connectors/power", "description": "CEE 7/16, Europlug" }, - { "name": "Type D", "category": "connectors/power", "description": "BS 546, India 5A/15A" }, - { "name": "Type E", "category": "connectors/power", "description": "CEE 7/5, French 2 pin" }, - { "name": "Type F", "category": "connectors/power", "description": "CEE 7/4, Schuko" }, - { "name": "Type E/F", "category": "connectors/power", "description": "CEE 7/7, Schuko/French hybrid" }, - { "name": "Type G", "category": "connectors/power", "description": "BS 1363, U.K. 3 pin" }, - { "name": "Type H", "category": "connectors/power", "description": "SI 32 Israel"}, - { "name": "Type I", "category": "connectors/power", "description": "AS/NZS 3112, Australia 3 pin" }, - { "name": "Type J", "category": "connectors/power", "description": "SEV 1011, Swiss 3 pin" }, - { "name": "Type K", "category": "connectors/power", "description": "DS 60884-2-D1, Danish 3 pin" }, - { "name": "Type L", "category": "connectors/power", "description": "CEI 23-16/VII, Italian 3 pin" } - ], - "properties": [ - { "name": "max current", "description": "Current rating"}, - { "name": "max power", "description": "Power rating" }, - { "name": "grid frequency", "description": "Typical frequency" } - ] -} \ No newline at end of file diff --git a/backend/shared_data/it.json b/backend/shared_data/it.json deleted file mode 100644 index 56f6189..0000000 --- a/backend/shared_data/it.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "depends": [ "git:electrical" ], - "categories": [ - { "name": "pc", "description": "PC related" }, - { "name": "usb", "parent": "connectors" } - ], - "tags": [ - { "name": "case", "category": "pc" }, - { "name": "cooler", "category": "pc" }, - { "name": "cpu" }, - { "name": "drone" }, - { "name": "fan", "category": "pc" }, - { "name": "gpu" }, - { "name": "hdd" }, - { "name": "headset" }, - { "name": "hub" }, - { "name": "iot" }, - { "name": "keyboard" }, - { "name": "laptop" }, - { "name": "memory" }, - { "name": "microphone" }, - { "name": "monitor" }, - { "name": "motherboard" }, - { "name": "mouse" }, - { "name": "pc"}, - { "name": "power supply" }, - { "name": "printer" }, - { "name": "router" }, - { "name": "scanner" }, - { "name": "server" }, - { "name": "smartphone" }, - { "name": "smartwatch" }, - { "name": "speaker" }, - { "name": "ssd" }, - { "name": "switch" }, - { "name": "tablet" }, - { "name": "watercooling" , "category": "pc"}, - { "name": "webcam" }, - { "name": "workstation" }, - { "name": "USB", "category": "connectors" }, - { "name": "Type A", "category": "connectors/usb" }, - { "name": "Type B", "category": "connectors/usb" }, - { "name": "Type C", "category": "connectors/usb" }, - { "name": "Micro", "category": "connectors/usb" }, - { "name": "Mini", "category": "connectors/usb" }, - { "name": "USB 2.0", "category": "connectors/usb" }, - { "name": "USB 3.0", "category": "connectors/usb" }, - { "name": "USB 3.1", "category": "connectors/usb" }, - { "name": "USB 3.2", "category": "connectors/usb" }, - { "name": "OTG", "category": "connectors/usb" }, - { "name": "thunderbolt", "category": "connectors/usb" }, - { "name": "24pin", "category": "connectors" }, - { "name": "8pin", "category": "connectors" }, - { "name": "atx", "category": "connectors" }, - { "name": "chinch", "category": "connectors" }, - { "name": "displayport", "category": "connectors" }, - { "name": "dvi", "category": "connectors" }, - { "name": "eps", "category": "connectors" }, - { "name": "floppy", "category": "connectors" }, - { "name": "hdmi", "category": "connectors" }, - { "name": "ide", "category": "connectors" }, - { "name": "jack", "category": "connectors" }, - { "name": "m.2", "category": "connectors" }, - { "name": "molex", "category": "connectors" }, - { "name": "p4", "category": "connectors" }, - { "name": "pcie", "category": "connectors" }, - { "name": "qsfp", "category": "connectors" }, - { "name": "qsfp+", "category": "connectors" }, - { "name": "qsfp28", "category": "connectors" }, - { "name": "rj11", "category": "connectors" }, - { "name": "rj45", "category": "connectors" }, - { "name": "sas", "category": "connectors" }, - { "name": "sata", "category": "connectors" }, - { "name": "scsi", "category": "connectors" }, - { "name": "sfp", "category": "connectors" }, - { "name": "sfp+", "category": "connectors" }, - { "name": "sfp28", "category": "connectors" }, - { "name": "toslink", "category": "connectors" }, - { "name": "vga", "category": "connectors" }, - { "name": "xlr", "category": "connectors" } - ] -} \ No newline at end of file diff --git a/backend/shared_data/screws.json b/backend/shared_data/screws.json deleted file mode 100644 index 20c8b93..0000000 --- a/backend/shared_data/screws.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "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/shared_data/tools.json b/backend/shared_data/tools.json deleted file mode 100644 index d4c4f88..0000000 --- a/backend/shared_data/tools.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "depends": ["git:base"], - "categories": [ - { "name": "powertools", "parent": "tools" } - ], - "tags": [ - { "name": "3d printer"}, - { "name": "air compressor", "category": "powertools"}, - { "name": "air filter"}, - { "name": "automotive", "category": "tools" }, - { "name": "bandsaw"}, - { "name": "belt sander"}, - { "name": "bench grinder"}, - { "name": "circular saw", "category": "powertools"}, - { "name": "concrete", "category": "tools" }, - { "name": "construction", "category": "tools" }, - { "name": "corded", "category": "powertools" }, - { "name": "cordless", "category": "powertools" }, - { "name": "disc sander"}, - { "name": "drill press"}, - { "name": "drywall", "category": "tools" }, - { "name": "dust collector"}, - { "name": "dust mask"}, - { "name": "ear protection"}, - { "name": "electrical", "category": "tools" }, - { "name": "eye protection"}, - { "name": "fire extinguisher"}, - { "name": "first aid"}, - { "name": "generator", "category": "powertools"}, - { "name": "glue gun", "category": "powertools"}, - { "name": "grinder", "category": "powertools"}, - { "name": "hammer drill", "category": "powertools"}, - { "name": "handheld", "category": "tools" }, - { "name": "heat gun", "category": "powertools"}, - { "name": "impact driver", "category": "powertools"}, - { "name": "jigsaw"}, - { "name": "jointer"}, - { "name": "ladder"}, - { "name": "laser cutter"}, - { "name": "lathe"}, - { "name": "masonry", "category": "tools" }, - { "name": "mechanical", "category": "tools" }, - { "name": "metalworking", "category": "tools" }, - { "name": "mill"}, - { "name": "multitool"}, - { "name": "nailgun", "category": "powertools"}, - { "name": "oscillating tool", "category": "powertools"}, - { "name": "painting", "category": "tools" }, - { "name": "planer"}, - { "name": "plasma cutter"}, - { "name": "plumbing", "category": "tools" }, - { "name": "powerplane", "category": "powertools"}, - { "name": "pressure washer", "category": "powertools"}, - { "name": "roofing", "category": "tools" }, - { "name": "router table"}, - { "name": "router", "category": "powertools"}, - { "name": "sawhorse"}, - { "name": "sawzall", "category": "powertools"}, - { "name": "screwgun", "category": "powertools"}, - { "name": "stationary", "category": "tools" }, - { "name": "tablesaw"}, - { "name": "tile", "category": "tools" }, - { "name": "welder", "category": "powertools"}, - { "name": "woodworking", "category": "tools" }, - { "name": "workbench"}, - { "name": "worklight"} - ] -} diff --git a/backend/toolshed/admin.py b/backend/toolshed/admin.py index bbd1525..aeefc0c 100644 --- a/backend/toolshed/admin.py +++ b/backend/toolshed/admin.py @@ -1,39 +1,11 @@ from django.contrib import admin -from toolshed.models import ( - InventoryItem, ItemProperty, ItemTag, Property, Tag, Category, StorageLocation, WorkflowInstance, -) - - -class ItemTagInline(admin.TabularInline): - model = ItemTag - extra = 0 - autocomplete_fields = ('tag',) - - -class ItemPropertyInline(admin.TabularInline): - model = ItemProperty - extra = 0 - autocomplete_fields = ('property',) +from toolshed.models import InventoryItem, Property, Tag, Category class InventoryItemAdmin(admin.ModelAdmin): - list_display = ('name', 'description', 'category', 'availability_policy', 'owned_quantity', 'owner', - 'storage_location', 'get_tags', 'get_properties') - search_fields = ('name', 'description', 'category__name', 'availability_policy', 'owner__username', - 'storage_location__name', 'tags__name', 'itemproperty__property__name') - inlines = (ItemTagInline, ItemPropertyInline) - - def get_queryset(self, request): - return super().get_queryset(request).prefetch_related('tags', 'itemproperty_set__property') - - @admin.display(description='Tags') - def get_tags(self, obj): - return ', '.join(tag.name for tag in obj.tags.all()) - - @admin.display(description='Properties') - def get_properties(self, obj): - return ', '.join(f'{ip.property.name}={ip.value}' for ip in obj.itemproperty_set.all()) + list_display = ('name', 'description', 'category', 'availability_policy', 'owned_quantity', 'owner') + search_fields = ('name', 'description', 'category', 'availability_policy', 'owned_quantity', 'owner') admin.site.register(InventoryItem, InventoryItemAdmin) @@ -41,7 +13,7 @@ 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__name', 'unit_symbol', 'origin') + search_fields = ('name', 'description', 'category', 'unit_symbol', 'base2_prefix', 'dimensions', 'origin') admin.site.register(Property, PropertyAdmin) @@ -49,7 +21,7 @@ admin.site.register(Property, PropertyAdmin) class TagAdmin(admin.ModelAdmin): list_display = ('name', 'description', 'category', 'origin') - search_fields = ('name', 'description', 'category__name', 'origin') + search_fields = ('name', 'description', 'category', 'origin') admin.site.register(Tag, TagAdmin) @@ -57,26 +29,7 @@ admin.site.register(Tag, TagAdmin) class CategoryAdmin(admin.ModelAdmin): list_display = ('name', 'description', 'parent', 'origin') - search_fields = ('name', 'description', 'parent__name', 'origin') + search_fields = ('name', 'description', 'parent', 'origin') admin.site.register(Category, CategoryAdmin) - - -class StorageLocationAdmin(admin.ModelAdmin): - list_display = ('name', 'description', 'category', 'parent', 'owner') - search_fields = ('name', 'description', 'category__name', 'parent__name', 'owner__username') - list_filter = ('category', 'owner') - - -admin.site.register(StorageLocation, StorageLocationAdmin) - - -class WorkflowInstanceAdmin(admin.ModelAdmin): - list_display = ('name', 'state', 'owner', 'created_at', 'updated_at') - search_fields = ('name', 'owner__username') - list_filter = ('state', 'created_at', 'owner') - readonly_fields = ('created_at', 'updated_at') - - -admin.site.register(WorkflowInstance, WorkflowInstanceAdmin) diff --git a/backend/toolshed/api/inventory.py b/backend/toolshed/api/inventory.py index 5846e03..c39a5c8 100644 --- a/backend/toolshed/api/inventory.py +++ b/backend/toolshed/api/inventory.py @@ -1,14 +1,14 @@ from django.db import transaction from django.urls import path -from rest_framework import routers, viewsets, status -from rest_framework.decorators import authentication_classes, api_view, permission_classes, action +from rest_framework import routers, viewsets +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 toolshed.models import InventoryItem, StorageLocation, WorkflowInstance -from toolshed.serializers import InventoryItemSerializer, StorageLocationSerializer, WorkflowInstanceSerializer +from toolshed.models import InventoryItem, StorageLocation +from toolshed.serializers import InventoryItemSerializer, StorageLocationSerializer router = routers.SimpleRouter() @@ -22,8 +22,7 @@ def inventory_items(identity): except ToolshedUser.DoesNotExist: pass for friend in identity.friends.all(): - friend_user = friend.user.first() - if friend_user: + if friend_user := friend.user.get(): for item in friend_user.inventory_items.all(): if item.availability_policy != 'private': yield item @@ -53,25 +52,13 @@ class InventoryItemViewSet(viewsets.ModelViewSet): instance.delete() -def matches_query(item, query): - query = query.lower() - if query in item.name.lower(): - return True - if item.description and query in item.description.lower(): - return True - if any(query in tag.name.lower() for tag in item.tags.all()): - return True - return False - - @api_view(['GET']) @authentication_classes([SignatureAuthentication]) @permission_classes([IsAuthenticated]) def search_inventory_items(request): query = request.query_params.get('query') if query: - matching_items = [item for item in inventory_items(request.user) if matches_query(item, query)] - return Response(InventoryItemSerializer(matching_items, many=True).data) + return Response(InventoryItemSerializer(inventory_items(request.user), many=True).data) return Response({'error': 'No query provided.'}, status=400) @@ -85,47 +72,9 @@ class StorageLocationViewSet(viewsets.ModelViewSet): return StorageLocation.objects.filter(owner=self.request.user.user.get()) return StorageLocation.objects.none() - def perform_create(self, serializer): - with transaction.atomic(): - serializer.save(owner=self.request.user.user.get()) - - def perform_update(self, serializer): - with transaction.atomic(): - if serializer.instance.owner == self.request.user.user.get(): - serializer.save() - - def perform_destroy(self, instance): - if instance.owner == self.request.user.user.get(): - instance.delete() - - -class WorkflowInstanceViewSet(viewsets.ModelViewSet): - serializer_class = WorkflowInstanceSerializer - authentication_classes = [SignatureAuthentication] - permission_classes = [IsAuthenticated] - - def get_queryset(self): - if type(self.request.user) == KnownIdentity and self.request.user.user.exists(): - return WorkflowInstance.objects.filter(owner=self.request.user.user.get()) - return WorkflowInstance.objects.none() - - def perform_create(self, serializer): - with transaction.atomic(): - serializer.save(owner=self.request.user.user.get()) - - def perform_update(self, serializer): - with transaction.atomic(): - if serializer.instance.owner == self.request.user.user.get(): - serializer.save() - - def perform_destroy(self, instance): - if instance.owner == self.request.user.user.get(): - instance.delete() - router.register(r'inventory_items', InventoryItemViewSet, basename='inventory_items') router.register(r'storage_locations', StorageLocationViewSet, basename='storage_locations') -router.register(r'workflows', WorkflowInstanceViewSet, basename='workflows') urlpatterns = router.urls + [ path('search/', search_inventory_items, name='search_inventory_items'), diff --git a/backend/toolshed/api/offlinedata.py b/backend/toolshed/api/offlinedata.py deleted file mode 100644 index 6fdd571..0000000 --- a/backend/toolshed/api/offlinedata.py +++ /dev/null @@ -1,253 +0,0 @@ -from django.http import HttpResponse -from django.urls import path -from rest_framework.decorators import api_view, permission_classes, authentication_classes -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response - -from authentication.signature_auth import SignatureAuthentication -from toolshed.offlinedata import ( - inventory_rows, friend_rows, location_rows, inventory_files, rows_to_csv, - import_locations, import_friends, import_inventory, - profile_data, profile_picture_files, settings_data, import_profile, import_settings, - delete_user_data, delete_user_account, -) - - -def local_user_or_none(identity): - """Resolve the local ToolshedUser associated with an authenticated KnownIdentity, if any. - - Returns None if the identity belongs to an external/remote user with no local account. - """ - if identity is None: - return None - return identity.user.first() - - -def user_data(user): - import io - import json - import zipfile - - zip_buffer = io.BytesIO() - - with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file: - zip_file.writestr('profile.json', json.dumps(profile_data(user))) - zip_file.writestr('settings.json', json.dumps(settings_data(user))) - - inventory_csv = b''.join(rows_to_csv(inventory_rows(user))) - zip_file.writestr('inventory.csv', inventory_csv) - - friends_csv = b''.join(rows_to_csv(friend_rows(user))) - zip_file.writestr('friends.csv', friends_csv) - - locations_csv = b''.join(rows_to_csv(location_rows(user))) - zip_file.writestr('locations.csv', locations_csv) - - written_files = set() - for arcname, data in profile_picture_files(user): - zip_file.writestr(arcname, data) - written_files.add(arcname) - - for arcname, data in inventory_files(user): - if arcname in written_files: - continue - zip_file.writestr(arcname, data) - written_files.add(arcname) - - return zip_buffer.getvalue() - - -def parse_user_data(data): - import io - import zipfile - - with zipfile.ZipFile(io.BytesIO(data), "r") as zip_file: - for file_name in zip_file.namelist(): - yield file_name, zip_file.read(file_name) - - -def import_files(zip_file): - """Fault-tolerant extraction of the 'files/' subfolder into File objects. - - Returns a dict mapping the zip arcname to the created/existing File instance. Entries that - fail to read or save are silently skipped so a single corrupt attachment doesn't abort the import. - """ - from hashlib import sha256 - import mimetypes - - from django.core.files.base import ContentFile - from django.db import transaction - from files.models import File - - result = {} - for name in zip_file.namelist(): - if not name.startswith('files/') or name == 'files/': - continue - try: - with transaction.atomic(): - data = zip_file.read(name) - content_hash = sha256(data).hexdigest() - mime_type, _ = mimetypes.guess_type(name) - file_obj = File.objects.filter(hash=content_hash).first() - if file_obj is None: - file_obj = File.objects.create( - file=ContentFile(data, content_hash), - mime_type=mime_type or 'application/octet-stream', - hash=content_hash, - ) - result[name] = file_obj - except Exception as error: - print(f'Skipping file "{name}" during import: {error}') - return result - - - -def import_user_data(user, data): - """Fault-tolerant import of an export zip produced by `user_data()`. - - Any of 'profile.json', 'settings.json', 'inventory.csv', 'friends.csv', 'locations.csv' or - the 'files/' subfolder may be missing; whatever is present is imported and everything else - is silently skipped. - - Each section is imported inside its own transaction savepoint (`transaction.atomic()`), so a - DB-level failure in one section (e.g. a profile.json whose email collides with another - account) can't leave the connection in a broken/aborted-transaction state that would - otherwise silently take down every subsequent section (including the inventory items and - their properties) with an opaque "current transaction is aborted" error. - """ - import io - import zipfile - - from django.db import transaction - - summary = {'profile': False, 'settings': 0, 'locations': 0, 'friends': 0, 'inventory_items': 0, 'files': 0, - 'errors': []} - - with zipfile.ZipFile(io.BytesIO(data), 'r') as zip_file: - names = set(zip_file.namelist()) - - available_files = import_files(zip_file) - summary['files'] = len(available_files) - - if 'profile.json' in names: - try: - with transaction.atomic(): - summary['profile'] = import_profile(user, zip_file.read('profile.json'), available_files) - except Exception as error: - summary['errors'].append(f'Could not import profile.json: {error}') - - if 'settings.json' in names: - try: - with transaction.atomic(): - summary['settings'] = import_settings(user, zip_file.read('settings.json')) - except Exception as error: - summary['errors'].append(f'Could not import settings.json: {error}') - - if 'locations.csv' in names: - try: - with transaction.atomic(): - summary['locations'] = import_locations(user, zip_file.read('locations.csv')) - except Exception as error: - summary['errors'].append(f'Could not import locations.csv: {error}') - - if 'friends.csv' in names: - try: - with transaction.atomic(): - summary['friends'] = import_friends(user, zip_file.read('friends.csv')) - except Exception as error: - summary['errors'].append(f'Could not import friends.csv: {error}') - - if 'inventory.csv' in names: - try: - with transaction.atomic(): - summary['inventory_items'], inventory_errors = import_inventory( - user, zip_file.read('inventory.csv'), available_files) - summary['errors'].extend(inventory_errors) - except Exception as error: - summary['errors'].append(f'Could not import inventory.csv: {error}') - - return summary - - - -def _extract_zip_bytes(zip_payload): - """Normalize the incoming 'zip' request payload (uploaded file, base64 string, or raw bytes).""" - import base64 - - if hasattr(zip_payload, 'read'): - return zip_payload.read() - if isinstance(zip_payload, str): - try: - return base64.b64decode(zip_payload, validate=True) - except Exception: - return zip_payload.encode('utf-8') - return zip_payload - - -@api_view(['POST']) -@permission_classes([IsAuthenticated]) -@authentication_classes([SignatureAuthentication]) -def import_data(request, format=None): - local_user = local_user_or_none(request.user) - if local_user is None: - return Response({'detail': 'This endpoint is only available to local users'}, status=403) - zip_payload = request.data.get('zip') - if not zip_payload: - return Response(status=400) - try: - zip_bytes = _extract_zip_bytes(zip_payload) - summary = import_user_data(local_user, zip_bytes) - except Exception as error: - return Response({'detail': f'Could not read zip file: {error}'}, status=400) - return Response(summary, status=200) - - -@api_view(['GET']) -@permission_classes([IsAuthenticated]) -@authentication_classes([SignatureAuthentication]) -def export_data(request, format=None): - local_user = local_user_or_none(request.user) - if local_user is None: - return Response({'detail': 'This endpoint is only available to local users'}, status=403) - return HttpResponse(user_data(local_user), content_type='application/zip', status=200) - - -@api_view(['DELETE']) -@permission_classes([IsAuthenticated]) -@authentication_classes([SignatureAuthentication]) -def delete_data(request, format=None): - """Wipe all of the local user's data (everything included in the export), but keep the account. - - This is *not* account deletion/closure - the user stays logged in and can keep using the - account (with all their data reset to a blank slate) afterwards. - """ - local_user = local_user_or_none(request.user) - if local_user is None: - return Response({'detail': 'This endpoint is only available to local users'}, status=403) - summary = delete_user_data(local_user) - return Response(summary, status=200) - - -@api_view(['DELETE']) -@permission_classes([IsAuthenticated]) -@authentication_classes([SignatureAuthentication]) -def delete_account(request, format=None): - """Permanently close the local user's account, after wiping all of its data. - - Unlike `delete_data()`, this also removes the account itself - the user can no longer log - in afterwards. Their public identity is kept so remote friends/history referencing it stay - intact, but the local ToolshedUser row is gone. - """ - local_user = local_user_or_none(request.user) - if local_user is None: - return Response({'detail': 'This endpoint is only available to local users'}, status=403) - summary = delete_user_account(local_user) - return Response(summary, status=200) - - -urlpatterns = [ - path('export/', export_data, name='export_data'), - path('import/', import_data, name='import_data'), - path('account_data/', delete_data, name='delete_data'), - path('account/', delete_account, name='delete_account'), -] diff --git a/backend/toolshed/migrations/0007_workflowinstance.py b/backend/toolshed/migrations/0007_workflowinstance.py deleted file mode 100644 index 7ecc834..0000000 --- a/backend/toolshed/migrations/0007_workflowinstance.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 4.2.2 on 2025-09-26 10:20 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('toolshed', '0006_alter_tag_options_alter_category_name_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='WorkflowInstance', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255)), - ('state', models.CharField(max_length=255)), - ('payload', models.JSONField(blank=True, default=dict)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workflows', to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/backend/toolshed/migrations/0008_alter_inventoryitem_storage_location.py b/backend/toolshed/migrations/0008_alter_inventoryitem_storage_location.py deleted file mode 100644 index 4c089a4..0000000 --- a/backend/toolshed/migrations/0008_alter_inventoryitem_storage_location.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.2 on 2026-07-23 02:02 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('toolshed', '0007_workflowinstance'), - ] - - operations = [ - migrations.AlterField( - model_name='inventoryitem', - name='storage_location', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inventory_items', to='toolshed.storagelocation'), - ), - ] diff --git a/backend/toolshed/models.py b/backend/toolshed/models.py index 47a790c..2dd0713 100644 --- a/backend/toolshed/models.py +++ b/backend/toolshed/models.py @@ -1,5 +1,5 @@ from django.db import models -from django.core.validators import MinValueValidator, MaxValueValidator +from django.core.validators import MinValueValidator from django_softdelete.models import SoftDeleteModel from rest_framework.exceptions import ValidationError @@ -26,10 +26,6 @@ class Category(SoftDeleteModel): parent = str(self.parent) + "/" if self.parent else "" return parent + self.name - def get_handle(self): - """Return a fully qualified handle like 'git:base#category:tools'""" - return f"{self.origin}#category:{self.name}" - class Property(models.Model): name = models.CharField(max_length=255) @@ -54,10 +50,6 @@ class Property(models.Model): def __str__(self): return self.name - def get_handle(self): - """Return a fully qualified handle like 'git:base#property:length'""" - return f"{self.origin}#property:{self.name}" - class Tag(models.Model): name = models.CharField(max_length=255) @@ -77,10 +69,6 @@ class Tag(models.Model): def __str__(self): return self.name - def get_handle(self): - """Return a fully qualified handle like 'git:tools#tag:drill'""" - return f"{self.origin}#tag:{self.name}" - class InventoryItem(SoftDeleteModel): AVAILABILITY_POLICY_CHOICES = ( @@ -102,7 +90,7 @@ class InventoryItem(SoftDeleteModel): tags = models.ManyToManyField(Tag, through='ItemTag', related_name='inventory_items') properties = models.ManyToManyField(Property, through='ItemProperty') files = models.ManyToManyField(File, related_name='connected_items') - storage_location = models.ForeignKey('StorageLocation', on_delete=models.SET_NULL, null=True, blank=True, + storage_location = models.ForeignKey('StorageLocation', on_delete=models.CASCADE, null=True, blank=True, related_name='inventory_items') def clean(self): @@ -132,17 +120,3 @@ class StorageLocation(models.Model): def __str__(self): parent = str(self.parent) + "/" if self.parent else "" return parent + self.name - - -class WorkflowInstance(models.Model): - name = models.CharField(max_length=255) - state = models.CharField(max_length=255) - payload = models.TextField(default='', blank=True) # an opaque, frontend-serialized JSON string on the backend. - owner = models.ForeignKey(ToolshedUser, on_delete=models.CASCADE, related_name='workflows') - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __str__(self): - return f"{self.name} ({self.state})" - - diff --git a/backend/toolshed/offlinedata.py b/backend/toolshed/offlinedata.py deleted file mode 100644 index 9a6c5f5..0000000 --- a/backend/toolshed/offlinedata.py +++ /dev/null @@ -1,670 +0,0 @@ -"""Data helpers for building/importing a user's offline export. - -These functions deal with the toolshed models and CSV row shapes only - they know nothing -about the zip container format or how the request is authenticated. See toolshed/api/offlinedata.py -for the zip-building/parsing and the API views themselves. -""" - - -def inventory_rows(user): - """Generator that yields the given user's inventory items as flattened dicts, one per row.""" - import mimetypes - - from toolshed.models import InventoryItem - - items = (InventoryItem.objects - .filter(owner=user) - .select_related('category', 'storage_location') - .prefetch_related('tags', 'itemproperty_set__property', 'files')) - - for item in items: - file_paths = [] - for f in item.files.all(): - extension = mimetypes.guess_extension(f.mime_type) or '' - file_paths.append(f'files/{f.hash}{extension}') - - yield { - 'id': item.id, - 'name': item.name or '', - 'description': item.description or '', - 'category': item.category.get_handle() if item.category else '', - 'availability_policy': item.availability_policy, - 'owned_quantity': item.owned_quantity, - 'storage_location': str(item.storage_location) if item.storage_location else '', - 'tags': ', '.join(tag.get_handle() for tag in item.tags.all()), - 'properties': _encode_properties_cell(item.itemproperty_set.all()), - 'files': ', '.join(file_paths), - 'created_at': item.created_at.isoformat() if item.created_at else '', - } - - -def friend_rows(user): - """Generator that yields the given user's friends (known identities) as flattened dicts, one per row.""" - friends = user.friends.all() - - for friend in friends: - yield { - 'username': friend.username, - 'domain': friend.domain, - 'handle': f'{friend.username}@{friend.domain}', - 'public_key': friend.public_key, - } - - -def location_path(location): - """Recursively build the '/'-joined path of a StorageLocation, following its parent chain.""" - if location.parent: - return location_path(location.parent) + '/' + location.name - return location.name - - -def location_rows(user): - """Generator that yields the given user's storage locations as flattened dicts, one per row.""" - from toolshed.models import StorageLocation - - locations = StorageLocation.objects.filter(owner=user).select_related('category', 'parent') - - for location in locations: - yield { - 'id': location.id, - 'name': location.name, - 'description': location.description or '', - 'category': str(location.category) if location.category else '', - 'parent': str(location.parent) if location.parent else '', - 'path': location_path(location), - } - - -def inventory_files(user): - """Generator that yields (arcname, data) for each unique file attached to the user's inventory items. - - Files are deduplicated by hash and placed under a 'files/' subfolder, keeping their original - extension (guessed from mime_type) so attachments and images remain viewable once extracted. - """ - import mimetypes - - from toolshed.models import InventoryItem - - seen_hashes = set() - items = InventoryItem.objects.filter(owner=user).prefetch_related('files') - - for item in items: - for file in item.files.all(): - if file.hash in seen_hashes: - continue - seen_hashes.add(file.hash) - - extension = mimetypes.guess_extension(file.mime_type) or '' - arcname = f'files/{file.hash}{extension}' - - file.file.open('rb') - try: - data = file.file.read() - finally: - file.file.close() - - yield arcname, data - - -def profile_data(user): - """Return the given user's profile as a dict, matching profile.json in the export zip.""" - import mimetypes - - data = { - 'username': user.username, - 'domain': user.domain, - 'email': user.email, - 'first_name': user.first_name or '', - 'last_name': user.last_name or '', - 'profile_picture': None, - } - if user.profile_picture: - extension = mimetypes.guess_extension(user.profile_picture.mime_type) or '' - data['profile_picture'] = f'files/{user.profile_picture.hash}{extension}' - return data - - -def profile_picture_files(user): - """Generator that yields (arcname, data) for the user's profile picture, if one is set. - - Kept separate from `inventory_files()` so the profile picture is included in the export - even for users with no inventory items or whose picture isn't attached to any item. - """ - import mimetypes - - if not user.profile_picture: - return - - extension = mimetypes.guess_extension(user.profile_picture.mime_type) or '' - arcname = f'files/{user.profile_picture.hash}{extension}' - - user.profile_picture.file.open('rb') - try: - data = user.profile_picture.file.read() - finally: - user.profile_picture.file.close() - - yield arcname, data - - -def settings_data(user): - """Return the given user's account-level preferences as a {key: value} dict for settings.json. - - Only account preferences (AccountPreference) are exported; device-level preferences stay - in the browser's local storage since they describe the device, not the account. - """ - return {pref.key: pref.value for pref in user.preferences.all()} - - -def import_profile(user, data, available_files): - """Fault-tolerant import of profile.json, updating the user's editable profile fields. - - Only 'first_name', 'last_name', 'email' and 'profile_picture' (a 'files/...' path present in - `available_files`) are applied; 'username' and 'domain' are ignored since they identify the - account itself and can't be changed by an import. - """ - import json - - try: - profile = json.loads(data.decode('utf-8')) - if not isinstance(profile, dict): - return False - - if 'first_name' in profile: - user.first_name = profile.get('first_name') or '' - if 'last_name' in profile: - user.last_name = profile.get('last_name') or '' - if profile.get('email'): - user.email = profile['email'] - - picture_path = profile.get('profile_picture') - if picture_path and picture_path in available_files: - user.profile_picture = available_files[picture_path] - - user.save() - return True - except Exception as error: - print(f'Skipping profile.json: {error}') - return False - - -def import_settings(user, data): - """Fault-tolerant import of settings.json into the user's account-level preferences. - - Each top-level key/value pair is upserted as an AccountPreference; unreadable data or a - non-object payload is skipped rather than aborting the whole import. - """ - import json - - from authentication.models import AccountPreference - - try: - settings = json.loads(data.decode('utf-8')) - if not isinstance(settings, dict): - return 0 - - imported = 0 - for key, value in settings.items(): - try: - AccountPreference.objects.update_or_create(user=user, key=key, defaults={'value': value}) - imported += 1 - except Exception as error: - print(f'Skipping setting "{key}": {error}') - return imported - except Exception as error: - print(f'Skipping settings.json: {error}') - return 0 - - -def delete_user_data(user): - """Permanently delete everything that `user_data()` exports, keeping the account itself intact. - - This removes the user's inventory items (hard delete, bypassing soft-delete), storage - locations, account preferences, the friends relation on their public identity, the profile - picture, and any File blobs (profile picture / inventory attachments) that would otherwise be - orphaned - i.e. not referenced by any other inventory item or user. Files still referenced - elsewhere (they're deduplicated by content hash) are left untouched. The ToolshedUser account - (and its underlying KnownIdentity) is *not* deleted - this only wipes the account's data, it - doesn't close the account. See `delete_account()` in toolshed/api/offlinedata.py for that. - - Returns a summary dict describing what was removed. - """ - from django.db import transaction - - from toolshed.models import InventoryItem, StorageLocation - - summary = {'inventory_items': 0, 'locations': 0, 'settings': 0, 'friends': 0, 'files': 0} - - with transaction.atomic(): - candidate_file_ids = set( - InventoryItem.global_objects.filter(owner=user).values_list('files__id', flat=True)) - candidate_file_ids.discard(None) - if user.profile_picture_id: - candidate_file_ids.add(user.profile_picture_id) - - for item in InventoryItem.global_objects.filter(owner=user): - item.hard_delete() - summary['inventory_items'] += 1 - - summary['locations'], _ = StorageLocation.objects.filter(owner=user).delete() - summary['settings'], _ = user.preferences.all().delete() - - summary['friends'] = user.public_identity.friends.count() - user.public_identity.friends.clear() - - user.profile_picture = None - user.save(update_fields=['profile_picture']) - - summary['files'] = _delete_orphaned_files(candidate_file_ids) - - - return summary - - -def delete_user_account(user): - """Permanently delete the local user's account, after wiping all of its data. - - This first calls `delete_user_data()` to remove everything covered by the data export - (inventory, locations, settings, friends relation, profile picture and orphaned files), - then deletes the ToolshedUser row itself. The underlying KnownIdentity is kept so remote - friends/history relating to this identity remain intact - only the local account is closed. - - Returns a summary dict describing what was removed, with `account` set to True. - """ - from django.db import transaction - - with transaction.atomic(): - summary = delete_user_data(user) - user.delete() - summary['account'] = True - - return summary - - -def _delete_orphaned_files(file_ids): - """Delete File rows (and their underlying blobs) in `file_ids` that are no longer referenced. - - A File is considered orphaned once no InventoryItem and no ToolshedUser (profile picture) - references it anymore. Returns the number of files deleted. - """ - from files.models import File - - deleted = 0 - for file_obj in File.objects.filter(id__in=file_ids): - if file_obj.connected_items.exists() or file_obj.profile_picture_users.exists(): - continue - file_obj.file.delete(save=False) - file_obj.delete() - deleted += 1 - return deleted - - -def rows_to_csv(rows, fieldnames=None, encoding='utf-8'): - """Generator that consumes an iterable of dicts and yields encoded CSV data chunk by chunk.""" - import csv - - class _Echo: - """A file-like object whose write() just returns what was passed, for streaming csv.writer output.""" - - def write(self, value): - return value - - rows = iter(rows) - try: - first_row = next(rows) - except StopIteration: - return - - if fieldnames is None: - fieldnames = list(first_row.keys()) - - writer = csv.DictWriter(_Echo(), fieldnames=fieldnames) - yield writer.writeheader().encode(encoding) - yield writer.writerow(first_row).encode(encoding) - for row in rows: - yield writer.writerow(row).encode(encoding) - - -def _read_csv_rows(data, encoding='utf-8'): - """Decode CSV bytes and yield rows as dicts keyed by the header labels (not column offsets).""" - import csv - import io - - reader = csv.DictReader(io.StringIO(data.decode(encoding))) - for row in reader: - yield row - - -def _field_is_optional(model, field_name): - """Return True if a field may be omitted: it's a relation, has a default, or allows null/blank.""" - try: - field = model._meta.get_field(field_name) - except Exception: - return True - if getattr(field, 'many_to_many', False) or getattr(field, 'one_to_many', False): - return True - return bool(getattr(field, 'null', False) or getattr(field, 'blank', False) or field.has_default()) - - -def get_or_create_category(path): - """Resolve or create a Category from a '/'-separated path such as 'Electronics/Cables'.""" - from toolshed.models import Category - - parent = None - category = None - for part in (p for p in path.split('/') if p): - category, _ = Category.objects.get_or_create(name=part, parent=parent, defaults={'origin': 'import'}) - parent = category - return category - - -def import_locations(user, data): - """Fault-tolerant import of locations.csv into StorageLocations owned by `user`. - - Rows are read by header label. Rows missing the required 'name' column are skipped. - Optional columns ('description', 'category') are simply omitted if absent. Locations are - processed in path-depth order so a child's parent already exists by the time it's needed. - Each row runs in its own transaction savepoint so a DB-level failure on one row (e.g. a - constraint violation) can't poison the surrounding transaction and silently break every - subsequent row. - """ - from django.db import transaction - - from toolshed.models import StorageLocation - - rows = list(_read_csv_rows(data)) - rows.sort(key=lambda row: (row.get('path') or row.get('name') or '').count('/')) - - resolved_by_path = {} - imported = 0 - - for row in rows: - try: - with transaction.atomic(): - name = (row.get('name') or '').strip() - if not name and not _field_is_optional(StorageLocation, 'name'): - continue # required column missing, skip this row - - path = (row.get('path') or name).strip() - parent = None - if '/' in path: - parent = resolved_by_path.get(path.rsplit('/', 1)[0]) - - category = None - category_path = row.get('category') - if category_path: - category = get_or_create_category(category_path) - - location, _ = StorageLocation.objects.update_or_create( - owner=user, name=name, parent=parent, - defaults={ - 'description': row.get('description', '') or '', - 'category': category, - }, - ) - resolved_by_path[path] = location - imported += 1 - except Exception as error: - print(f'Skipping location row {row}: {error}') - - return imported - - -def import_friends(user, data): - """Fault-tolerant import of friends.csv, adding valid entries to the user's known friends.""" - from django.db import transaction - - from authentication.models import KnownIdentity - - imported = 0 - for row in _read_csv_rows(data): - try: - with transaction.atomic(): - username = row.get('username') - domain = row.get('domain') - handle = row.get('handle') - if (not username or not domain) and handle and '@' in handle: - username, domain = handle.split('@', 1) - - public_key = row.get('public_key') - if not username or not domain or not public_key: - continue # required fields missing, skip this row - - identity, _ = KnownIdentity.objects.get_or_create( - username=username, domain=domain, - defaults={'public_key': public_key}, - ) - user.public_identity.friends.add(identity) - imported += 1 - except Exception as error: - print(f'Skipping friend row {row}: {error}') - - return imported - - -class _HandleNotFound(Exception): - """Raised when a fully qualified handle (e.g. 'git:base#tag:drill') can't be resolved. - - This intentionally aborts the whole row (rather than falling back to creating a new - entity), since a handle references a *specific* entity from a *specific* origin - silently - creating a new local entity named after the raw handle string would be incorrect. - """ - - -def _resolve_handle(value, entity_type, model): - """Resolve a fully qualified handle (e.g. 'git:base#tag:drill') to an *existing* model instance. - - Never creates anything: a handle references a specific entity from a specific origin, so - silently creating a new local entity named after the raw handle string would be incorrect. - Raises `_HandleNotFound` if the handle's entity type doesn't match or no such object exists, - so the caller can skip the row and report a helpful error instead. - """ - origin, rest = value.split('#', 1) - if ':' in rest: - found_type, name = rest.split(':', 1) - if found_type != entity_type: - raise _HandleNotFound( - f"expected a {entity_type} handle but got '{value}' (type '{found_type}')") - else: - name = rest - try: - return model.objects.get(origin=origin, name=name) - except model.DoesNotExist: - raise _HandleNotFound(f"{entity_type} '{value}' does not exist, skipping item") - - -def _quote_value_if_needed(value): - """Wrap `value` in double quotes (CSV-style, doubling any embedded quotes) if it contains a - comma or a quote character, so it survives sitting inside a comma-separated "handle=value" - list unambiguously. - """ - if any(ch in value for ch in ',"'): - return '"' + value.replace('"', '""') + '"' - return value - - -def _encode_properties_cell(item_properties): - """Encode an item's properties as a comma-separated "handle=value" list for the 'properties' - CSV cell, quoting a value (CSV-style) when it contains a comma or a quote character so it - still round-trips correctly. See `_parse_properties_cell()` for the reader side. - """ - entries = [ - f"{ip.property.get_handle()}={_quote_value_if_needed(ip.value or '')}" - for ip in item_properties - ] - return ', '.join(entries) - - -def _split_quoted_comma_list(raw_value): - """Split a comma-separated list into entries, honouring double-quoted substrings (CSV-style, - wherever they appear in an entry) so a quoted value's own commas aren't mistaken for - separators. Doubled quotes ("") inside a quoted substring are unescaped to a single literal - quote, and the surrounding quotes themselves are stripped from the result. - - Only the single space after each ", " separator (as written by `_encode_properties_cell()`) - is dropped - unlike a blanket `.strip()`, this preserves any leading/trailing whitespace that - is genuinely part of a (quoted) value. - """ - entries = [] - current = [] - in_quotes = False - i = 0 - length = len(raw_value) - - while i < length: - char = raw_value[i] - if char == '"': - if in_quotes and i + 1 < length and raw_value[i + 1] == '"': - current.append('"') - i += 2 - continue - in_quotes = not in_quotes - i += 1 - continue - if char == ',' and not in_quotes: - entries.append(''.join(current)) - current = [] - i += 1 - if i < length and raw_value[i] == ' ': - i += 1 # skip the single space of the ", " separator - continue - current.append(char) - i += 1 - - entries.append(''.join(current)) - return [entry for entry in entries if entry] - - -def _parse_properties_cell(raw_value, resolve_property): - """Parse the 'properties' CSV cell into a list of (Property, value) tuples. - - The cell is a comma-separated list of "handle=value" entries; a value containing a comma or - a quote character is double-quoted (CSV-style) by `_encode_properties_cell()` so it survives - the round trip intact. - """ - raw_value = (raw_value or '').strip() - if not raw_value: - return [] - - properties = [] - for prop_entry in _split_quoted_comma_list(raw_value): - if '=' not in prop_entry: - continue - prop_name, value = prop_entry.split('=', 1) - prop = resolve_property(prop_name.strip()) - if prop: - properties.append((prop, value)) - - return properties - - - -def import_inventory(user, data, available_files): - """Fault-tolerant import of inventory.csv into InventoryItems owned by `user`. - - `available_files` maps the 'files' column's paths to File instances successfully extracted - from the zip; references to missing files are ignored rather than failing the row. - - If a row references a fully qualified tag/property/category handle that doesn't exist - locally, the whole item is skipped (rather than creating a bogus local entity named after - the raw handle) and a helpful message is added to the returned `errors` list. Each row runs - in its own transaction savepoint so a DB-level failure on one row can't poison the - surrounding transaction and silently break every subsequent row. - """ - from django.db import transaction - - from toolshed.models import Category, InventoryItem, ItemProperty, StorageLocation, Tag, Property - - def resolve_category_from_csv(value): - """Resolve a category from CSV - supports both old format and fully qualified handles""" - if not value: - return None - if '#' in value: - return _resolve_handle(value, 'category', Category) - # Fallback to old path-based lookup - return get_or_create_category(value) - - def resolve_tag_from_csv(value): - """Resolve a tag from CSV - supports both old format and fully qualified handles""" - if not value: - return None - if '#' in value: - return _resolve_handle(value, 'tag', Tag) - # Fallback to old name-only lookup or create - return Tag.objects.get_or_create(name=value, category=None, defaults={'origin': 'import'})[0] - - def resolve_property_from_csv(value): - """Resolve a property from CSV - supports both old format and fully qualified handles""" - if not value: - return None - if '#' in value: - return _resolve_handle(value, 'property', Property) - # Fallback to old name-only lookup or create - return Property.objects.get_or_create(name=value, category=None, defaults={'origin': 'import'})[0] - - imported = 0 - errors = [] - for row in _read_csv_rows(data): - name = (row.get('name') or '').strip() - try: - with transaction.atomic(): - file_paths = [p.strip() for p in (row.get('files') or '').split(',') if p.strip()] - files = [available_files[p] for p in file_paths if p in available_files] - - if not name and not files: - continue # InventoryItem requires a name or at least one file - - category = resolve_category_from_csv((row.get('category') or '').strip()) - - tags = [] - for tag_name in (row.get('tags') or '').split(','): - tag_name = tag_name.strip() - if tag_name: - tag = resolve_tag_from_csv(tag_name) - if tag: - tags.append(tag) - - properties = _parse_properties_cell(row.get('properties') or '', resolve_property_from_csv) - - storage_location = None - location_path_value = row.get('storage_location') - if location_path_value: - storage_location = StorageLocation.objects.filter( - owner=user, name=location_path_value.rsplit('/', 1)[-1]).first() - - try: - owned_quantity = int(row.get('owned_quantity') or 1) - except (TypeError, ValueError): - owned_quantity = 1 - - item = InventoryItem.objects.create( - owner=user, - name=name or None, - description=row.get('description', '') or '', - category=category, - availability_policy=row.get('availability_policy') or 'private', - owned_quantity=owned_quantity, - storage_location=storage_location, - ) - - for tag in tags: - item.tags.add(tag, through_defaults={}) - - for prop, value in properties: - ItemProperty.objects.create(inventory_item=item, property=prop, value=value) - - for file in files: - item.files.add(file) - - imported += 1 - except _HandleNotFound as error: - message = f"Skipping item '{name or 'unnamed'}': {error}" - print(message) - errors.append(message) - except Exception as error: - message = f'Skipping inventory row {row}: {error}' - print(message) - errors.append(message) - - return imported, errors - diff --git a/backend/toolshed/serializers.py b/backend/toolshed/serializers.py index 43c335b..73edeff 100644 --- a/backend/toolshed/serializers.py +++ b/backend/toolshed/serializers.py @@ -3,47 +3,7 @@ from authentication.models import KnownIdentity, ToolshedUser, FriendRequestInco 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, WorkflowInstance - - -def parse_handle(handle): - """Parse a fully qualified handle like 'git:base#property:length' into (origin, entity_type, name)""" - if '#' not in handle: - # Fallback to old format (just name) - return None, None, handle - origin, rest = handle.split('#', 1) - if ':' not in rest: - return origin, None, rest - entity_type, name = rest.split(':', 1) - return origin, entity_type, name - - -def resolve_category_handle(handle): - """Resolve a fully qualified handle to a Category object""" - origin, entity_type, name = parse_handle(handle) - if origin and entity_type == 'category': - return Category.objects.get(origin=origin, name=name) - # Fallback to name-only lookup - return Category.objects.get(name=handle.split('/')[-1]) - - -def resolve_property_handle(handle): - """Resolve a fully qualified handle to a Property object""" - origin, entity_type, name = parse_handle(handle) - if origin and entity_type == 'property': - return Property.objects.get(origin=origin, name=name) - # Fallback to name-only lookup - return Property.objects.get(name=handle) - - -def resolve_tag_handle(handle): - """Resolve a fully qualified handle to a Tag object""" - origin, entity_type, name = parse_handle(handle) - if origin and entity_type == 'tag': - return Tag.objects.get(origin=origin, name=name) - # Fallback to name-only lookup - return Tag.objects.get(name=handle) - +from toolshed.models import Category, Property, ItemProperty, InventoryItem, Tag, StorageLocation class FriendSerializer(serializers.ModelSerializer): @@ -69,45 +29,33 @@ class FriendRequestSerializer(serializers.ModelSerializer): class PropertySerializer(serializers.ModelSerializer): - category = serializers.SerializerMethodField() - handle = serializers.SerializerMethodField() - - def get_category(self, obj): - return resolve_category_handle(obj.category.get_handle()) if obj.category else None - - def get_handle(self, obj): - return obj.get_handle() + category = serializers.SlugRelatedField(queryset=Category.objects.all(), slug_field='name') class Meta: model = Property - fields = ['name', 'description', 'category', 'unit_symbol', 'unit_name', 'unit_name_plural', 'base2_prefix', 'handle'] + fields = ['name', 'description', 'category', 'unit_symbol', 'unit_name', 'unit_name_plural', 'base2_prefix'] class CategorySerializer(serializers.ModelSerializer): - handle = serializers.SerializerMethodField() - class Meta: model = Category - fields = ['name', 'handle'] - - def get_handle(self, obj): - return obj.get_handle() + fields = ['name'] def to_representation(self, instance): - return instance.name + return str(instance) def to_internal_value(self, data): - return resolve_category_handle(data.split("/")[-1]) + return Category.objects.get(name=data.split("/")[-1]) class StorageLocationSerializer(serializers.ModelSerializer): owner = OwnerSerializer(read_only=True) - category = serializers.CharField(required=False, allow_null=True, allow_blank=True) + category = CategorySerializer(required=False, allow_null=True) path = serializers.SerializerMethodField() class Meta: model = StorageLocation - fields = ['id', 'name', 'description', 'path', 'category', 'owner', 'parent'] + fields = ['id', 'name', 'description', 'path', 'category', 'owner'] read_only_fields = ['path'] @staticmethod @@ -119,28 +67,23 @@ class StorageLocationSerializer(serializers.ModelSerializer): class ItemPropertySerializer(serializers.ModelSerializer): property = PropertySerializer(read_only=True) - handle = serializers.SerializerMethodField() class Meta: model = ItemProperty - fields = ['property', 'value', 'handle'] - - def get_handle(self, obj): - return obj.property.get_handle() + fields = ['property', 'value'] def to_representation(self, instance): return {'value': instance.value, 'name': instance.property.name} def to_internal_value(self, data): - prop = resolve_property_handle(data.get('name') or data.get('handle')) + prop = Property.objects.get(name=data['name']) value = data['value'] return {'property': prop, 'value': value} class InventoryItemSerializer(serializers.ModelSerializer): owner = OwnerSerializer(read_only=True) - tags = serializers.SerializerMethodField() - tags_input = serializers.ListField(child=serializers.CharField(), write_only=True, required=False) + tags = serializers.SlugRelatedField(many=True, required=False, queryset=Tag.objects.all(), slug_field='name') properties = ItemPropertySerializer(many=True, required=False, source='itemproperty_set') category = CategorySerializer(required=False, allow_null=True) files = FileSerializer(many=True, read_only=True) @@ -148,16 +91,11 @@ class InventoryItemSerializer(serializers.ModelSerializer): class Meta: model = InventoryItem fields = ['id', 'name', 'description', 'owner', 'category', 'availability_policy', 'owned_quantity', 'owner', - 'tags', 'tags_input', 'properties', 'files', 'storage_location'] - - def get_tags(self, obj): - return [tag.name for tag in obj.tags.all()] + 'tags', 'properties', 'files', 'storage_location'] def to_internal_value(self, data): files = data.pop('files', []) - tags_input = data.pop('tags_input', data.pop('tags', [])) ret = super().to_internal_value(data) - ret['tags'] = [resolve_tag_handle(tag) for tag in tags_input] ret['files'] = files return ret @@ -200,12 +138,3 @@ class InventoryItemSerializer(serializers.ModelSerializer): ItemProperty.objects.create(inventory_item=item, property=prop['property'], value=prop['value']) item.save() return item - -class WorkflowInstanceSerializer(serializers.ModelSerializer): - owner = serializers.StringRelatedField(read_only=True) - - class Meta: - model = WorkflowInstance - fields = ['id', 'name', 'state', 'payload', 'owner', 'created_at', 'updated_at'] - read_only_fields = ['owner', 'created_at', 'updated_at'] - diff --git a/backend/toolshed/tests/fixtures.py b/backend/toolshed/tests/fixtures.py index 79b573d..5ee9d8b 100644 --- a/backend/toolshed/tests/fixtures.py +++ b/backend/toolshed/tests/fixtures.py @@ -1,5 +1,4 @@ -from toolshed.models import Category, Tag, Property, InventoryItem, ItemProperty, StorageLocation, WorkflowInstance -import json +from toolshed.models import Category, Tag, Property, InventoryItem, ItemProperty, StorageLocation class CategoryTestMixin: @@ -55,33 +54,3 @@ class LocationTestMixin: self.f['loc3'] = StorageLocation.objects.create(name='loc3', owner=self.f['local_user1'], parent=self.f['loc1']) self.f['loc4'] = StorageLocation.objects.create(name='loc4', owner=self.f['local_user1'], parent=self.f['loc1'], category=self.f['cat1']) - - -class WorkflowTestMixin: - def prepare_workflows(self): - # `payload` is an opaque, frontend-serialized JSON string on the backend. - self.f['workflow1'] = WorkflowInstance.objects.create( - name='workflow1', - state='initial', - payload=json.dumps({}), - owner=self.f['local_user1'] - ) - self.f['workflow2'] = WorkflowInstance.objects.create( - name='workflow1', - state='upload', - payload=json.dumps({'files': ['ef35c4a9b2d1c4f1a3e6f7d8c9b0a1b2']}), - owner=self.f['local_user1'] - ) - self.f['workflow3'] = WorkflowInstance.objects.create( - name='workflow1', - state='describe', - payload=json.dumps({'files': ['ef35c4a9b2d1c4f1a3e6f7d8c9b0a1b2', 'a1b2c3d4e5f60718293a4b5c6d7e8f90', 'b1c2d3e4f5a60718293b4c5d6e7f8090'], - 'descriptions': ['file 1 description']}), - owner=self.f['local_user1'] - ) - self.f['workflow_user2'] = WorkflowInstance.objects.create( - name='workflow2', - state='initial', - payload=json.dumps({}), - owner=self.f['local_user2'] - ) diff --git a/backend/toolshed/tests/test_locations.py b/backend/toolshed/tests/test_locations.py index 5f9eef9..d792857 100644 --- a/backend/toolshed/tests/test_locations.py +++ b/backend/toolshed/tests/test_locations.py @@ -1,6 +1,6 @@ from authentication.tests import SignatureAuthClient, UserTestMixin, ToolshedTestCase from files.tests import FilesTestMixin -from toolshed.models import InventoryItem, Category, StorageLocation +from toolshed.models import InventoryItem, Category from toolshed.tests import InventoryTestMixin, LocationTestMixin client = SignatureAuthClient() @@ -69,63 +69,3 @@ class LocationApiTestCase(UserTestMixin, InventoryTestMixin, LocationTestMixin, self.assertEqual(reply.json()[3]['description'], None) self.assertEqual(reply.json()[3]['category'], 'cat1') self.assertEqual(reply.json()[3]['path'], 'loc1/loc4') - - def test_post_new_location(self): - reply = client.post('/api/storage_locations/', self.f['local_user1'], { - 'name': 'loc5', - 'description': 'a new location', - }) - self.assertEqual(reply.status_code, 201) - self.assertEqual(StorageLocation.objects.count(), 5) - location = StorageLocation.objects.get(name='loc5') - self.assertEqual(location.description, 'a new location') - self.assertEqual(location.owner, self.f['local_user1']) - self.assertEqual(location.parent, None) - self.assertEqual(reply.json()['path'], 'loc5') - - def test_post_new_nested_location(self): - reply = client.post('/api/storage_locations/', self.f['local_user1'], { - 'name': 'loc5', - 'parent': self.f['loc3'].id, - }) - self.assertEqual(reply.status_code, 201) - location = StorageLocation.objects.get(name='loc5') - self.assertEqual(location.parent, self.f['loc3']) - self.assertEqual(reply.json()['path'], 'loc1/loc3/loc5') - - def test_patch_location(self): - reply = client.patch('/api/storage_locations/' + str(self.f['loc2'].id) + '/', self.f['local_user1'], { - 'name': 'loc2-renamed', - 'parent': self.f['loc1'].id, - }) - self.assertEqual(reply.status_code, 200) - location = StorageLocation.objects.get(id=self.f['loc2'].id) - self.assertEqual(location.name, 'loc2-renamed') - self.assertEqual(location.parent, self.f['loc1']) - self.assertEqual(reply.json()['path'], 'loc1/loc2-renamed') - - def test_delete_location(self): - reply = client.delete('/api/storage_locations/' + str(self.f['loc4'].id) + '/', self.f['local_user1']) - self.assertEqual(reply.status_code, 204) - self.assertEqual(StorageLocation.objects.count(), 3) - self.assertEqual(StorageLocation.objects.filter(id=self.f['loc4'].id).count(), 0) - - def test_delete_location_with_items_sets_null(self): - item = InventoryItem.objects.create( - owner=self.f['local_user1'], name='located_item', storage_location=self.f['loc3']) - reply = client.delete('/api/storage_locations/' + str(self.f['loc3'].id) + '/', self.f['local_user1']) - self.assertEqual(reply.status_code, 204) - item.refresh_from_db() - self.assertIsNone(item.storage_location) - self.assertEqual(InventoryItem.objects.filter(id=item.id).count(), 1) - - def test_locations_are_owner_scoped(self): - reply = client.get('/api/storage_locations/', self.f['local_user2']) - self.assertEqual(reply.status_code, 200) - self.assertEqual(len(reply.json()), 0) - - def test_cannot_delete_other_users_location(self): - reply = client.delete('/api/storage_locations/' + str(self.f['loc1'].id) + '/', self.f['local_user2']) - self.assertEqual(reply.status_code, 404) - self.assertEqual(StorageLocation.objects.filter(id=self.f['loc1'].id).count(), 1) - diff --git a/backend/toolshed/tests/test_offlinedata.py b/backend/toolshed/tests/test_offlinedata.py deleted file mode 100644 index 499e597..0000000 --- a/backend/toolshed/tests/test_offlinedata.py +++ /dev/null @@ -1,264 +0,0 @@ -from django.core.files.base import ContentFile -from django.test import Client - -from authentication.models import AccountPreference, ToolshedUser -from authentication.tests import UserTestMixin, SignatureAuthClient, ToolshedTestCase -from files.models import File -from toolshed.models import InventoryItem, ItemProperty, StorageLocation -from toolshed.offlinedata import import_inventory, inventory_rows, rows_to_csv -from toolshed.tests import CategoryTestMixin, LocationTestMixin, PropertyTestMixin, TagTestMixin - -anonymous_client = Client() -client = SignatureAuthClient() - - -class _DeleteTestDataMixin(UserTestMixin, CategoryTestMixin, LocationTestMixin): - """Shared fixture setup for the delete-data and delete-account test cases.""" - - def setUp(self): - super().setUp() - self.prepare_users() - self.prepare_categories() - self.prepare_locations() - - self.f['local_user1'].friends.add(self.f['local_user2'].public_identity) - - self.f['shared_file'] = File.objects.create( - file=ContentFile(b'shared', 'shared'), mime_type='text/plain', hash='shared') - self.f['orphan_file'] = File.objects.create( - file=ContentFile(b'orphan', 'orphan'), mime_type='text/plain', hash='orphan') - - self.f['item1'] = InventoryItem.objects.create( - owner=self.f['local_user1'], owned_quantity=1, name='item1', category=self.f['cat1']) - self.f['item1'].files.add(self.f['orphan_file']) - - self.f['item_other_user'] = InventoryItem.objects.create( - owner=self.f['local_user2'], owned_quantity=1, name='item2', category=self.f['cat1']) - self.f['item_other_user'].files.add(self.f['shared_file']) - - self.f['item1'].files.add(self.f['shared_file']) - - AccountPreference.objects.create(user=self.f['local_user1'], key='theme', value='dark') - - self.f['local_user1'].profile_picture = self.f['orphan_file'] - self.f['local_user1'].save() - - -class DeleteDataTestCase(_DeleteTestDataMixin, ToolshedTestCase): - - def test_delete_data_anonymous(self): - response = anonymous_client.delete('/api/account_data/') - self.assertEqual(response.status_code, 403) - - def test_delete_data_removes_all_owned_data_but_keeps_account(self): - response = client.delete('/api/account_data/', self.f['local_user1']) - self.assertEqual(response.status_code, 200) - - summary = response.json() - self.assertEqual(summary['inventory_items'], 1) - self.assertEqual(summary['locations'], 4) - self.assertEqual(summary['settings'], 1) - self.assertEqual(summary['friends'], 1) - - # the account itself survives - this wipes data, it doesn't close the account - self.f['local_user1'].refresh_from_db() - self.assertTrue(ToolshedUser.objects.filter(username='testuser1').exists()) - self.assertIsNone(self.f['local_user1'].profile_picture) - - self.assertFalse(InventoryItem.global_objects.filter(owner_id=self.f['local_user1'].id).exists()) - self.assertFalse(StorageLocation.objects.filter(owner_id=self.f['local_user1'].id).exists()) - self.assertFalse(AccountPreference.objects.filter(user_id=self.f['local_user1'].id).exists()) - self.assertEqual(self.f['local_user1'].public_identity.friends.count(), 0) - - # orphaned file (only referenced by the deleted user/items) is gone - self.assertFalse(File.objects.filter(hash='orphan').exists()) - # file still referenced by the other user's item survives - self.assertTrue(File.objects.filter(hash='shared').exists()) - - # the other user's data and identity/friend relation to the deleted identity are untouched - self.f['local_user2'].refresh_from_db() - self.assertTrue(InventoryItem.objects.filter(owner=self.f['local_user2']).exists()) - - -class DeleteAccountTestCase(_DeleteTestDataMixin, ToolshedTestCase): - - def test_delete_account_anonymous(self): - response = anonymous_client.delete('/api/account/') - self.assertEqual(response.status_code, 403) - - def test_delete_account_removes_data_and_closes_account(self): - user1_id = self.f['local_user1'].id - identity_id = self.f['local_user1'].public_identity_id - - response = client.delete('/api/account/', self.f['local_user1']) - self.assertEqual(response.status_code, 200) - - summary = response.json() - self.assertEqual(summary['inventory_items'], 1) - self.assertEqual(summary['locations'], 4) - self.assertEqual(summary['settings'], 1) - self.assertEqual(summary['friends'], 1) - self.assertTrue(summary['account']) - - # the account itself is gone - self.assertFalse(ToolshedUser.objects.filter(id=user1_id).exists()) - self.assertFalse(InventoryItem.global_objects.filter(owner_id=user1_id).exists()) - self.assertFalse(StorageLocation.objects.filter(owner_id=user1_id).exists()) - self.assertFalse(AccountPreference.objects.filter(user_id=user1_id).exists()) - - # the underlying identity is kept, so remote friends/history referencing it stay intact - from authentication.models import KnownIdentity - self.assertTrue(KnownIdentity.objects.filter(id=identity_id).exists()) - - # orphaned file (only referenced by the deleted user/items) is gone - self.assertFalse(File.objects.filter(hash='orphan').exists()) - # file still referenced by the other user's item survives - self.assertTrue(File.objects.filter(hash='shared').exists()) - - # the other user's data and identity/friend relation to the deleted identity are untouched - self.f['local_user2'].refresh_from_db() - self.assertTrue(InventoryItem.objects.filter(owner=self.f['local_user2']).exists()) - self.assertEqual(self.f['local_user2'].public_identity.friends.count(), 0) - - -class ImportInventoryPropertiesTestCase(UserTestMixin, CategoryTestMixin, TagTestMixin, PropertyTestMixin, - ToolshedTestCase): - """Properties must round-trip through export/import even when their value contains a - comma or an '=' sign - characters that a naive "handle=value, handle2=value2" encoding of - the 'properties' CSV cell would misinterpret as a field/entry separator. - """ - - def setUp(self): - super().setUp() - self.prepare_users() - self.prepare_categories() - self.prepare_tags() - self.prepare_properties() - - def test_property_values_with_comma_and_equals_round_trip(self): - item = InventoryItem.objects.create(owner=self.f['local_user1'], name='widget') - ItemProperty.objects.create(inventory_item=item, property=self.f['prop1'], value='10cm, 20cm') - ItemProperty.objects.create(inventory_item=item, property=self.f['prop2'], value='a=b') - - csv_bytes = b''.join(rows_to_csv(list(inventory_rows(self.f['local_user1'])))) - - imported, errors = import_inventory(self.f['local_user2'], csv_bytes, available_files={}) - self.assertEqual(errors, []) - self.assertEqual(imported, 1) - - new_item = InventoryItem.objects.get(owner=self.f['local_user2'], name='widget') - values = {ip.property.name: ip.value for ip in new_item.itemproperty_set.select_related('property')} - self.assertEqual(values, {'prop1': '10cm, 20cm', 'prop2': 'a=b'}) - - def test_legacy_comma_equals_format_is_still_importable(self): - handle1 = self.f['prop1'].get_handle() - handle2 = self.f['prop2'].get_handle() - csv_data = ( - 'name,properties\r\n' - f'legacy widget,"{handle1}=value1, {handle2}=value2"\r\n' - ).encode('utf-8') - - imported, errors = import_inventory(self.f['local_user1'], csv_data, available_files={}) - self.assertEqual(errors, []) - self.assertEqual(imported, 1) - - item = InventoryItem.objects.get(owner=self.f['local_user1'], name='legacy widget') - values = {ip.property.name: ip.value for ip in item.itemproperty_set.select_related('property')} - self.assertEqual(values, {'prop1': 'value1', 'prop2': 'value2'}) - - def test_item_without_properties_imports_cleanly(self): - item = InventoryItem.objects.create(owner=self.f['local_user1'], name='bare item') - - csv_bytes = b''.join(rows_to_csv(list(inventory_rows(self.f['local_user1'])))) - - imported, errors = import_inventory(self.f['local_user2'], csv_bytes, available_files={}) - self.assertEqual(errors, []) - self.assertEqual(imported, 1) - - new_item = InventoryItem.objects.get(owner=self.f['local_user2'], name='bare item') - self.assertEqual(list(new_item.itemproperty_set.all()), []) - - def test_category_and_tags_round_trip(self): - item = InventoryItem.objects.create( - owner=self.f['local_user1'], name='cat and tags item', category=self.f['cat1']) - item.tags.add(self.f['tag1'], self.f['tag2'], through_defaults={}) - - csv_bytes = b''.join(rows_to_csv(list(inventory_rows(self.f['local_user1'])))) - - imported, errors = import_inventory(self.f['local_user2'], csv_bytes, available_files={}) - self.assertEqual(errors, []) - self.assertEqual(imported, 1) - - new_item = InventoryItem.objects.get(owner=self.f['local_user2'], name='cat and tags item') - self.assertEqual(new_item.category, self.f['cat1']) - self.assertEqual(sorted(t.name for t in new_item.tags.all()), ['tag1', 'tag2']) - - def test_unknown_property_handle_skips_item_with_error(self): - csv_data = ( - 'name,properties\r\n' - 'ghost widget,test#property:doesnotexist=x\r\n' - ).encode('utf-8') - - imported, errors = import_inventory(self.f['local_user1'], csv_data, available_files={}) - self.assertEqual(imported, 0) - self.assertEqual(len(errors), 1) - self.assertIn('doesnotexist', errors[0]) - self.assertFalse(InventoryItem.objects.filter(owner=self.f['local_user1'], name='ghost widget').exists()) - - def test_property_value_with_quote_character_round_trips(self): - item = InventoryItem.objects.create(owner=self.f['local_user1'], name='quoted widget') - ItemProperty.objects.create(inventory_item=item, property=self.f['prop1'], value='12" screen') - - csv_bytes = b''.join(rows_to_csv(list(inventory_rows(self.f['local_user1'])))) - - imported, errors = import_inventory(self.f['local_user2'], csv_bytes, available_files={}) - self.assertEqual(errors, []) - self.assertEqual(imported, 1) - - new_item = InventoryItem.objects.get(owner=self.f['local_user2'], name='quoted widget') - values = {ip.property.name: ip.value for ip in new_item.itemproperty_set.select_related('property')} - self.assertEqual(values, {'prop1': '12" screen'}) - - -class ExportImportApiRoundTripTestCase(UserTestMixin, CategoryTestMixin, TagTestMixin, PropertyTestMixin, - ToolshedTestCase): - """End-to-end coverage of the /api/export/ + /api/import/ endpoints (as actually used by - clients), rather than calling the internal helper functions directly - this is what a real - export/import round trip between two accounts looks like. - """ - - def setUp(self): - super().setUp() - self.prepare_users() - self.prepare_categories() - self.prepare_tags() - self.prepare_properties() - - def test_export_then_import_preserves_category_tags_and_properties(self): - import base64 - - item = InventoryItem.objects.create( - owner=self.f['local_user1'], name='drill', description='cordless drill', - category=self.f['cat1'], availability_policy='friends', owned_quantity=2) - item.tags.add(self.f['tag1'], self.f['tag2'], through_defaults={}) - ItemProperty.objects.create(inventory_item=item, property=self.f['prop1'], value='10cm, 20cm') - ItemProperty.objects.create(inventory_item=item, property=self.f['prop2'], value='a=b') - - export_reply = client.get('/api/export/', self.f['local_user1']) - self.assertEqual(export_reply.status_code, 200) - zip_bytes = export_reply.content - - import_reply = client.post('/api/import/', self.f['local_user2'], - {'zip': base64.b64encode(zip_bytes).decode('ascii')}) - self.assertEqual(import_reply.status_code, 200) - summary = import_reply.json() - self.assertEqual(summary['inventory_items'], 1) - self.assertEqual(summary['errors'], []) - - new_item = InventoryItem.objects.get(owner=self.f['local_user2'], name='drill') - self.assertEqual(new_item.category, self.f['cat1']) - self.assertEqual(sorted(t.name for t in new_item.tags.all()), ['tag1', 'tag2']) - - values = {ip.property.name: ip.value for ip in new_item.itemproperty_set.select_related('property')} - self.assertEqual(values, {'prop1': '10cm, 20cm', 'prop2': 'a=b'}) - diff --git a/backend/toolshed/tests/test_workflow_api.py b/backend/toolshed/tests/test_workflow_api.py deleted file mode 100644 index 98489df..0000000 --- a/backend/toolshed/tests/test_workflow_api.py +++ /dev/null @@ -1,48 +0,0 @@ -import json -from django.test import Client -from django.urls import reverse -from rest_framework import status -from authentication.tests import UserTestMixin, SignatureAuthClient, ToolshedTestCase -from toolshed.tests import WorkflowTestMixin -from toolshed.models import WorkflowInstance - -anonymous_client = Client() -client = SignatureAuthClient() - - -class WorkflowInstanceApiTestCase(UserTestMixin, WorkflowTestMixin, ToolshedTestCase): - """Comprehensive test cases for the Workflow API""" - - def setUp(self): - super().setUp() - self.prepare_users() - self.prepare_workflows() - - - def test_get_workflow_instances(self): - reply = client.get('/api/workflows/', self.f['local_user1']) - self.assertEqual(reply.status_code, status.HTTP_200_OK) - self.assertEqual(len(reply.data), 3) - self.assertEqual(reply.data[0]['name'], 'workflow1') - self.assertEqual(reply.data[1]['name'], 'workflow1') - self.assertEqual(reply.data[2]['name'], 'workflow1') - self.assertEqual(reply.data[0]['state'], 'initial') - self.assertEqual(reply.data[1]['state'], 'upload') - self.assertEqual(reply.data[2]['state'], 'describe') - self.assertEqual(json.loads(reply.data[0]['payload']), {}) - self.assertEqual(json.loads(reply.data[1]['payload']), {'files': ['ef35c4a9b2d1c4f1a3e6f7d8c9b0a1b2']}) - self.assertEqual(json.loads(reply.data[2]['payload']), {'files': ['ef35c4a9b2d1c4f1a3e6f7d8c9b0a1b2', - 'a1b2c3d4e5f60718293a4b5c6d7e8f90', - 'b1c2d3e4f5a60718293b4c5d6e7f8090'], - 'descriptions': ['file 1 description']}) - - - - def test_get_workflow_instances_user2(self): - reply = client.get('/api/workflows/', self.f['local_user2']) - self.assertEqual(reply.status_code, status.HTTP_200_OK) - self.assertEqual(len(reply.data), 1) - self.assertEqual(reply.data[0]['name'], 'workflow2') - self.assertEqual(reply.data[0]['state'], 'initial') - self.assertEqual(json.loads(reply.data[0]['payload']), {}) - diff --git a/cli-client/toolshed-client.py b/cli-client/toolshed-client.py deleted file mode 100755 index 2c5d434..0000000 --- a/cli-client/toolshed-client.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import os -import requests -from nacl.signing import SigningKey -from json import dumps - - -class ToolshedApi: - user = None - host = None - signing_key = None - - def __init__(self, user, host, key): - if host is None: - raise ValueError("TOOLSHED_HOST environment variable not set") - - if user is None: - raise ValueError("TOOLSHED_USER environment variable not set") - - if key is None: - raise ValueError("TOOLSHED_KEY environment variable not set") - - if len(key) != 64: - raise ValueError("TOOLSHED_KEY must be 64 hex characters") - - signing_key = SigningKey(bytes.fromhex(key)) - - self.user = user - self.host = host - self.signing_key = signing_key - - def get(self, target): - url = "http://" + self.host + target - signed = self.signing_key.sign(url.encode('utf-8')) - signature = signed.signature.hex() - response = requests.get(url, headers={"Authorization": "Signature " + self.user + ":" + signature}) - return response.json() - - def post(self, target, data): - url = "http://" + self.host + target - json = dumps(data) - signed = self.signing_key.sign(url.encode('utf-8') + json.encode('utf-8')) - signature = signed.signature.hex() - response = requests.post(url, headers={"Authorization": "Signature " + self.user + ":" + signature}, json=data) - return response.json() - - -def main(): - host = os.environ.get('TOOLSHED_HOST') - user = os.environ.get('TOOLSHED_USER') - key = os.environ.get('TOOLSHED_KEY') - - parser = argparse.ArgumentParser(description='Toolshed API client') - parser.add_argument('--host', help='Toolshed host') - parser.add_argument('--user', help='Toolshed user') - parser.add_argument('--key', help='Toolshed key') - parser.add_argument('cmd', help='Command') - args = parser.parse_args() - - if args.host is not None: - host = args.host - - if args.user is not None: - user = args.user - - if args.key is not None: - key = args.key - - api = ToolshedApi(user, host, key) - - if args.cmd == 'getinventory': - inv = api.get("/api/inventory_items/") - print(inv) - elif args.cmd == 'additem': - inv = api.post("/api/inventory_items/", {"name": "test"}) - print(inv) - else: - print("Unknown command: " + args.cmd) - - -if __name__ == '__main__': - main() diff --git a/deploy/dev/Dockerfile.backend b/deploy/dev/Dockerfile.backend index d83b688..4eb1ded 100644 --- a/deploy/dev/Dockerfile.backend +++ b/deploy/dev/Dockerfile.backend @@ -13,4 +13,4 @@ COPY requirements.txt /code/ RUN pip install --no-cache-dir -r requirements.txt # Run the application -CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000 --insecure"] +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000", "--insecure"] diff --git a/deploy/dev/Dockerfile.frontend b/deploy/dev/Dockerfile.frontend index 07f7c49..85138e6 100644 --- a/deploy/dev/Dockerfile.frontend +++ b/deploy/dev/Dockerfile.frontend @@ -8,7 +8,6 @@ WORKDIR /app # A wildcard is used to ensure both package.json AND package-lock.json are copied COPY package.json ./ -COPY extras/ ./extras/ RUN npm install CMD [ "npm", "run", "dev", "--", "--host"] diff --git a/deploy/dev/instance_a/a.env b/deploy/dev/instance_a/a.env index 5816f2a..33c5b7e 100644 --- a/deploy/dev/instance_a/a.env +++ b/deploy/dev/instance_a/a.env @@ -2,8 +2,6 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG=True -SERVE_X_ACCEL_REDIRECT=False - # 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' diff --git a/deploy/dev/instance_a/nginx-a.dev.conf b/deploy/dev/instance_a/nginx-a.dev.conf index c661fb3..b77f550 100644 --- a/deploy/dev/instance_a/nginx-a.dev.conf +++ b/deploy/dev/instance_a/nginx-a.dev.conf @@ -14,7 +14,7 @@ http { } upstream dns { - server toolshed-dns:8053; + server dns:8053; } server { @@ -45,34 +45,8 @@ http { proxy_pass http://backend; } - location /media { - 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 /djangoadmin { - 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_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; + proxy_pass http://backend/docs; } location /static { @@ -106,35 +80,17 @@ http { # DoH server server { listen 5353 ssl; - server_name localhost 127.0.0.3; + server_name localhost; ssl_certificate /etc/nginx/nginx.crt; ssl_certificate_key /etc/nginx/nginx.key; - # Ensure CORS headers are present even when nginx generates 5xx responses. - add_header 'Access-Control-Allow-Origin' '*' always; - add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'Accept, Content-Type, Origin, User-Agent' always; - add_header 'Access-Control-Expose-Headers' 'Content-Type' always; - error_page 500 502 503 504 = @doh_error; - location /dns-query { - if ($request_method = OPTIONS) { - add_header 'Access-Control-Allow-Origin' '*' always; - add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'Accept, Content-Type, Origin, User-Agent' always; - add_header 'Access-Control-Max-Age' 86400 always; - add_header 'Content-Length' 0; - add_header 'Content-Type' 'text/plain; charset=utf-8'; - return 204; - } - proxy_pass http://dns; - } + # allow any origin + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS'; - location @doh_error { - default_type text/plain; - return 502 'DoH upstream unavailable'; } } } diff --git a/deploy/dev/instance_a/testdata.py b/deploy/dev/instance_a/testdata.py index 0e8f592..4a5cfbd 100644 --- a/deploy/dev/instance_a/testdata.py +++ b/deploy/dev/instance_a/testdata.py @@ -57,7 +57,7 @@ def create_test_data(): if KnownIdentity.objects.filter(username='test_b').exists(): identity = KnownIdentity.objects.get(username='test_b') identity = KnownIdentity.objects.create(username='test_b', domain='b.localhost', - public_key='14c44f03c3a0406934cf3a27b0eeedae8a08a3f436690c7103eca13435172a8c') + public_key='4b0dffe21764c591762615ef84cfea7fd4055fab3e9f58ae077fd8c79c34af91') # pk '2ec1e7d5f5b8d5f87233944970d57f942095fe9e6c4fc49edde61fcd3fb1bf40' identity.save() print('Created identity {}@{} with public key {}'.format(identity.username, identity.domain, diff --git a/deploy/dev/instance_b/b.env b/deploy/dev/instance_b/b.env index 76e8087..c0118ca 100644 --- a/deploy/dev/instance_b/b.env +++ b/deploy/dev/instance_b/b.env @@ -1,8 +1,6 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG=True -SERVE_X_ACCEL_REDIRECT=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' diff --git a/deploy/dev/instance_b/nginx-b.dev.conf b/deploy/dev/instance_b/nginx-b.dev.conf index e181011..bb6596c 100644 --- a/deploy/dev/instance_b/nginx-b.dev.conf +++ b/deploy/dev/instance_b/nginx-b.dev.conf @@ -35,28 +35,6 @@ http { proxy_pass http://backend; } - location /media { - 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 /redirect_media/ { - internal; - alias /var/www/userfiles/; - # This location serves the file directly, bypassing Django (and therefore - # django-cors-headers) entirely - it's the target of the X-Accel-Redirect - # response from files/media_urls.py, used when SERVE_X_ACCEL_REDIRECT=True. - # CORS headers must be added here explicitly since nothing else will. - add_header 'Access-Control-Allow-Origin' '*' always; - add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'Authorization, Accept, Content-Type, Origin, User-Agent' always; - } - location /docs { proxy_pass http://backend/docs; } diff --git a/deploy/dev/zone.json b/deploy/dev/zone.json index bbf8d6f..5c8be9a 100644 --- a/deploy/dev/zone.json +++ b/deploy/dev/zone.json @@ -21,4 +21,4 @@ "target": "127.0.0.2." } } -] \ No newline at end of file +] diff --git a/deploy/docker-compose.override.yml b/deploy/docker-compose.override.yml index cb3cdd3..6ed6978 100644 --- a/deploy/docker-compose.override.yml +++ b/deploy/docker-compose.override.yml @@ -10,7 +10,6 @@ services: - ../deploy/dev/instance_a/a.env:/code/.env - ../deploy/dev/instance_a/testdata.py:/code/testdata.py - ../deploy/dev/instance_a/a.sqlite3:/code/db.sqlite3 - - ../deploy/dev/instance_a/userfiles:/code/userfiles expose: - 8000 command: bash -c "python configure.py; python configure.py testdata; python manage.py runserver 0.0.0.0:8000 --insecure" @@ -24,7 +23,6 @@ services: - ../deploy/dev/instance_b/b.env:/code/.env - ../deploy/dev/instance_b/testdata.py:/code/testdata.py - ../deploy/dev/instance_b/b.sqlite3:/code/db.sqlite3 - - ../deploy/dev/instance_b/userfiles:/code/userfiles expose: - 8000 command: bash -c "python configure.py; python configure.py testdata; python manage.py runserver 0.0.0.0:8000 --insecure" @@ -59,7 +57,6 @@ services: - ./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 - - ./dev/instance_a/userfiles:/var/www/userfiles:ro ports: - "127.0.0.1:8080:8080" - "127.0.0.3:5353:5353" @@ -70,7 +67,6 @@ services: dockerfile: dev/Dockerfile.proxy volumes: - ./dev/instance_b/nginx-b.dev.conf:/etc/nginx/nginx.conf:ro - - ./dev/instance_b/userfiles:/var/www/userfiles:ro ports: - "127.0.0.2:8080:8080" @@ -82,7 +78,3 @@ services: - ./dev/zone.json:/dns/zone.json expose: - 8053 - networks: - default: - aliases: - - toolshed-dns diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..12187f1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,43 @@ +version: '3.3' + +services: + # db: + # image: postgres + # container_name: docker-django-vue-db + # environment: + # POSTGRES_USER: user + # POSTGRES_PASSWORD: pass + # POSTGRES_DB: db + # restart: unless-stopped + # ports: + # - "5432:5432" + django: + build: + context: ./backend + dockerfile: ./Dockerfile + command: python backend/manage.py runserver 0.0.0.0:8000 + volumes: + - .:/app + ports: + - "8002:8000" + networks: + - internal + # depends_on: + # - db + vue: + build: + context: ./frontend + dockerfile: ./Dockerfile + command: nginx -g 'daemon off;' + volumes: + - .:/app + ports: + - "8001:80" + networks: + - internal + - external + depends_on: + - django +networks: + external: + internal: \ No newline at end of file diff --git a/docs/development.md b/docs/development.md index b4e1dcd..3fcd229 100644 --- a/docs/development.md +++ b/docs/development.md @@ -97,12 +97,6 @@ Start the fullstack application: docker-compose -f deploy/docker-compose.override.yml up --build ``` -Run backend tests in Docker: - -``` bash -docker compose -f deploy/docker-compose.override.yml run --rm backend-a bash -lc "python configure.py && python manage.py test" -``` - 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. diff --git a/frontend/.local/RootCA.crt b/frontend/.local/RootCA.crt new file mode 100644 index 0000000..7e49ada --- /dev/null +++ b/frontend/.local/RootCA.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDLzCCAhegAwIBAgIUJmAh7fVmxDGKqLmRepxu/JqgEcgwDQYJKoZIhvcNAQEL +BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y +MzExMDMwMDI4NDNaFw0yNjA4MjMwMDI4NDNaMCcxCzAJBgNVBAYTAlVTMRgwFgYD +VQQDDA9FeGFtcGxlLVJvb3QtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQDE/lXJfHolXhbd7gevY0+AurY4iMAUx+FDbdpRaKc4z+hHANKWR5OZBwIj +mI6RNpe/yq+ZE7fDnQ5mq9BuD37i9ZR3TiFQQ0K6yVHJs1xn64Ce4dhz0LxIU+mR +rWYTMDkfjOnKi6ueycgKSzk6GnCgSix0ho4yRj8CPO9HzKf7bgHJZzh//xFk3Cbo +EgHGXxnm5yCfBthaU5Toey8AYlHlH/OVYTSxu2yRu/HdWbVglqdSu2us7U9SG88V +EeJf5fSwmyYS4ab9JZUhM7HgMVQZIx/Gq+VoYZCs6s8/Bm3hnFQDVHDQ9mAL9cZB +EvOgQGcEFXHS6SXkWwWpYietmNdPAgMBAAGjUzBRMB0GA1UdDgQWBBTCPiGSDMoi +nNiPFsW4fYZW3XDPKjAfBgNVHSMEGDAWgBTCPiGSDMoinNiPFsW4fYZW3XDPKjAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAQw65jeCi5mNPmw4Uw +BYEmGfzv7bYMrDu8w1A2U2yFa+6BN17mAMLJQxzcrvP4/DK1MaP1ehGfWRH2jzSb +m5CgbZYDVBib2jD1HlMFEBvEF2wduQL9mIipeYXFIS9ttuW7QVqso62Na/3tcp9J +dCfHsbnAvWSt63/7d9wHX3zlBN7v9GeJbXo14p8aA8IEjQFQWnkjhvRVzU/fltKm +dvG/xkXA59dqXMEYxHyGK2tE4WiaLwsQW44LQcuEjy4NSj5t0F4NWYVLUgIgnqk8 +RkGEXAxcRfFXSF6VEBTxoAAhTh5zSge4PSwHT+29dzkJqzpFb6cCke4Skk16KiW8 +0dg1 +-----END CERTIFICATE----- diff --git a/frontend/.local/RootCA.key b/frontend/.local/RootCA.key new file mode 100644 index 0000000..d47a3d1 --- /dev/null +++ b/frontend/.local/RootCA.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDE/lXJfHolXhbd +7gevY0+AurY4iMAUx+FDbdpRaKc4z+hHANKWR5OZBwIjmI6RNpe/yq+ZE7fDnQ5m +q9BuD37i9ZR3TiFQQ0K6yVHJs1xn64Ce4dhz0LxIU+mRrWYTMDkfjOnKi6ueycgK +Szk6GnCgSix0ho4yRj8CPO9HzKf7bgHJZzh//xFk3CboEgHGXxnm5yCfBthaU5To +ey8AYlHlH/OVYTSxu2yRu/HdWbVglqdSu2us7U9SG88VEeJf5fSwmyYS4ab9JZUh +M7HgMVQZIx/Gq+VoYZCs6s8/Bm3hnFQDVHDQ9mAL9cZBEvOgQGcEFXHS6SXkWwWp +YietmNdPAgMBAAECggEADKTgf77AHk7kO9lv39eJflM9p/JLFepgcJIH/cqwVQqv +N4N7gM7WT++tuPgtSm/NHK/XRoWH+YPGrVtg1Pekps+peGDo7Xxrt459c+tRxo/M +oSYQ3RosQ54zMzkEgL3LJNXIGcqOOPJtpw99/Fc/HGK2aMJMNTjiCXQ8zYh6zb38 +himWC1Vysb6tO4bQ/zJeMuh+ny6oAu0x8CAKJfFHwm/enA2ab2VGT9L/S/Mv7bqh +WmRsOrY7nnSSHhYFBr3VcvC4yzsQ+UI9LKllK1M2Lb7JVrGkgYwJFqiwomV5bRDn +QP4gFgDvgghsQgm9Fx7ZTC6vIXpgEZQAiSPkIVB7LQKBgQDiO9Db4Jp1h46Bu+je +JW3MUw8QNHRnlTmJr0bIu1IDq+3LBLJUo7wR2+/JxNx/v4R+qSRv65HEFF5QwuID +o+F2vZ08xHKY1uKuXV26VpRohuQVPz+PhUExYE29RhAUbGq/14wbH8vIpUNk5VKW +9yxAFZjqKyqhsSdL6C23i2AT6wKBgQDe6aDwRP0WXDsdgdiXiEXXZ47RyE6h9/8+ +ehx++rMHME2C6fL562MHBiwfZWoy3X/hlCahvOMGSPT93sqgKATYdYWzwQnOIyUG +LIH2ZfUStKnnK+yNMh4sUZZERDy+JSzvIULtxNJ/m4ai0FNTobAqw4WMXKUTKKOL +rSgRPkVFLQKBgQDVlQMpknJAYyn28SB4GX3/WKrv8ZWz9P3NLtz0xWQ4cyiccmKp +5aq4cZuAMYq6UGUV7zmnMA79vv0L4fDe3ukM4RaBODW4daasVxkVwod551+Nb5ji +NjvGEdVSy8KChZiCZUywjuQlGWD28El4nQH5vmoPKZGl9UnUL+55Sev64QKBgAOv +AqzVxwTmk0eCjMy8QMM1f+OxlrMQgRRMOkpS5EB7KNy/9vbwT9ue233NVDrw9BsL +MrQ7hdjlvb6wPtL0tlfJYWM+DmZsqED43VDwEGO1vpUGYctXh64dzwjjw8k0nhB5 +kTySoxPewxPPs4DPTDT4tC1Enqm5YzJ8swvN5qElAoGABPP5g+EekI2eo6xPIsVf +FPhto+WKn7Hxx+sQZBg7EuX8QLxrGBy9yeU0BMcyuvQYPHbGtfqlItVchBk7Nxug +s82jJgWYymbroKvCYShAnQujZuyjI62qJq1gfvfcLT4g+k3nZ+KZwCc4iusOELlH +0rnQsA6KSyCpKB51T1iUkZQ= +-----END PRIVATE KEY----- diff --git a/frontend/.local/RootCA.pem b/frontend/.local/RootCA.pem new file mode 100644 index 0000000..7e49ada --- /dev/null +++ b/frontend/.local/RootCA.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDLzCCAhegAwIBAgIUJmAh7fVmxDGKqLmRepxu/JqgEcgwDQYJKoZIhvcNAQEL +BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y +MzExMDMwMDI4NDNaFw0yNjA4MjMwMDI4NDNaMCcxCzAJBgNVBAYTAlVTMRgwFgYD +VQQDDA9FeGFtcGxlLVJvb3QtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQDE/lXJfHolXhbd7gevY0+AurY4iMAUx+FDbdpRaKc4z+hHANKWR5OZBwIj +mI6RNpe/yq+ZE7fDnQ5mq9BuD37i9ZR3TiFQQ0K6yVHJs1xn64Ce4dhz0LxIU+mR +rWYTMDkfjOnKi6ueycgKSzk6GnCgSix0ho4yRj8CPO9HzKf7bgHJZzh//xFk3Cbo +EgHGXxnm5yCfBthaU5Toey8AYlHlH/OVYTSxu2yRu/HdWbVglqdSu2us7U9SG88V +EeJf5fSwmyYS4ab9JZUhM7HgMVQZIx/Gq+VoYZCs6s8/Bm3hnFQDVHDQ9mAL9cZB +EvOgQGcEFXHS6SXkWwWpYietmNdPAgMBAAGjUzBRMB0GA1UdDgQWBBTCPiGSDMoi +nNiPFsW4fYZW3XDPKjAfBgNVHSMEGDAWgBTCPiGSDMoinNiPFsW4fYZW3XDPKjAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAQw65jeCi5mNPmw4Uw +BYEmGfzv7bYMrDu8w1A2U2yFa+6BN17mAMLJQxzcrvP4/DK1MaP1ehGfWRH2jzSb +m5CgbZYDVBib2jD1HlMFEBvEF2wduQL9mIipeYXFIS9ttuW7QVqso62Na/3tcp9J +dCfHsbnAvWSt63/7d9wHX3zlBN7v9GeJbXo14p8aA8IEjQFQWnkjhvRVzU/fltKm +dvG/xkXA59dqXMEYxHyGK2tE4WiaLwsQW44LQcuEjy4NSj5t0F4NWYVLUgIgnqk8 +RkGEXAxcRfFXSF6VEBTxoAAhTh5zSge4PSwHT+29dzkJqzpFb6cCke4Skk16KiW8 +0dg1 +-----END CERTIFICATE----- diff --git a/frontend/.local/RootCA.srl b/frontend/.local/RootCA.srl new file mode 100644 index 0000000..ea468e6 --- /dev/null +++ b/frontend/.local/RootCA.srl @@ -0,0 +1 @@ +0C2D2D6E0BCB8B8AFD205E5E4A616C11D96DD566 diff --git a/frontend/.local/domains.ext b/frontend/.local/domains.ext new file mode 100644 index 0000000..d2d1d41 --- /dev/null +++ b/frontend/.local/domains.ext @@ -0,0 +1,8 @@ +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names +[alt_names] +DNS.1 = localhost +#DNS.2 = fake1.local +#DNS.3 = fake2.local diff --git a/frontend/.local/fullchain.pem b/frontend/.local/fullchain.pem new file mode 100644 index 0000000..8df1930 --- /dev/null +++ b/frontend/.local/fullchain.pem @@ -0,0 +1,42 @@ +-----BEGIN CERTIFICATE----- +MIIDkjCCAnqgAwIBAgIUDC0tbgvLi4r9IF5eSmFsEdlt1WYwDQYJKoZIhvcNAQEL +BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y +MzExMDMwMDMxMDFaFw0yNjA4MjMwMDMxMDFaMG0xCzAJBgNVBAYTAlVTMRIwEAYD +VQQIDAlZb3VyU3RhdGUxETAPBgNVBAcMCFlvdXJDaXR5MR0wGwYDVQQKDBRFeGFt +cGxlLUNlcnRpZmljYXRlczEYMBYGA1UEAwwPbG9jYWxob3N0LmxvY2FsMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwUYH95U11TYvYn9or/cP/oPtiJJm +l1aH5J2AOfno/QW5FjDKeGUYeXrGTOLeDRiHIAHVtg00nfTtm56du5+I972VbEW6 +ngrQAGyk/GSvOGK1iG4M4eHpCkZHrdkubOWsO5vDj2juTwjE1fyncLmIbyZN5NKJ +6JID8c/ItFXQzXDLaFAQH3+uFFX9iTeT2cGyFEidooKo4oKj3imQRM6YKl4ygEMc +BTN0p7AqtjKSfva5WsMSXdESLsNdStgs5OgfJUE8x4jhcrVrZ2Mnw8Zmfb2pIoNe +6HphVtU96EMDqZmRJ4c9REqS6qZmHKsAPet/jyNNZjUJIptJE6Cl0IihhwIDAQAB +o3AwbjAfBgNVHSMEGDAWgBTCPiGSDMoinNiPFsW4fYZW3XDPKjAJBgNVHRMEAjAA +MAsGA1UdDwQEAwIE8DAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFIni +f5YpRjm9GEjktq2gr/34isB1MA0GCSqGSIb3DQEBCwUAA4IBAQCAv0FdWz+Ktq6+ +I7wOUWx3LHFQSov71ca9HGbD9SMOBbd26MJXnxxjOU62W+Q/Bkx4C3ezzSuLMhZq +Vl3jV2wDkWWCFvR1a2cwmr05B2xs/lqX+eNwrNZNfGxMV4I4dn+bkGe2czRcZaur +et0SylTQvWwrCgewRirS44LcN9EEutu0WzxlUpdty71zlVKdpyE6MrZ10ADcin5B +P/10++aBuQXF1EDFGZ6snexGuSsshAP2tBLww+Z8xPyN3SRMj/Y0z7Rgm4nhEfNP +M1BcUiaT7e8evsWVAyoDI6N1Z4M0p4SXLLeHdhKelbvNdeVZxwDkl8jR+Q5hwt7K +ZurEL1FZ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDLzCCAhegAwIBAgIUJmAh7fVmxDGKqLmRepxu/JqgEcgwDQYJKoZIhvcNAQEL +BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y +MzExMDMwMDI4NDNaFw0yNjA4MjMwMDI4NDNaMCcxCzAJBgNVBAYTAlVTMRgwFgYD +VQQDDA9FeGFtcGxlLVJvb3QtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQDE/lXJfHolXhbd7gevY0+AurY4iMAUx+FDbdpRaKc4z+hHANKWR5OZBwIj +mI6RNpe/yq+ZE7fDnQ5mq9BuD37i9ZR3TiFQQ0K6yVHJs1xn64Ce4dhz0LxIU+mR +rWYTMDkfjOnKi6ueycgKSzk6GnCgSix0ho4yRj8CPO9HzKf7bgHJZzh//xFk3Cbo +EgHGXxnm5yCfBthaU5Toey8AYlHlH/OVYTSxu2yRu/HdWbVglqdSu2us7U9SG88V +EeJf5fSwmyYS4ab9JZUhM7HgMVQZIx/Gq+VoYZCs6s8/Bm3hnFQDVHDQ9mAL9cZB +EvOgQGcEFXHS6SXkWwWpYietmNdPAgMBAAGjUzBRMB0GA1UdDgQWBBTCPiGSDMoi +nNiPFsW4fYZW3XDPKjAfBgNVHSMEGDAWgBTCPiGSDMoinNiPFsW4fYZW3XDPKjAP +BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAQw65jeCi5mNPmw4Uw +BYEmGfzv7bYMrDu8w1A2U2yFa+6BN17mAMLJQxzcrvP4/DK1MaP1ehGfWRH2jzSb +m5CgbZYDVBib2jD1HlMFEBvEF2wduQL9mIipeYXFIS9ttuW7QVqso62Na/3tcp9J +dCfHsbnAvWSt63/7d9wHX3zlBN7v9GeJbXo14p8aA8IEjQFQWnkjhvRVzU/fltKm +dvG/xkXA59dqXMEYxHyGK2tE4WiaLwsQW44LQcuEjy4NSj5t0F4NWYVLUgIgnqk8 +RkGEXAxcRfFXSF6VEBTxoAAhTh5zSge4PSwHT+29dzkJqzpFb6cCke4Skk16KiW8 +0dg1 +-----END CERTIFICATE----- diff --git a/frontend/.local/localhost.crt b/frontend/.local/localhost.crt new file mode 100644 index 0000000..ada652b --- /dev/null +++ b/frontend/.local/localhost.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDkjCCAnqgAwIBAgIUDC0tbgvLi4r9IF5eSmFsEdlt1WYwDQYJKoZIhvcNAQEL +BQAwJzELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD0V4YW1wbGUtUm9vdC1DQTAeFw0y +MzExMDMwMDMxMDFaFw0yNjA4MjMwMDMxMDFaMG0xCzAJBgNVBAYTAlVTMRIwEAYD +VQQIDAlZb3VyU3RhdGUxETAPBgNVBAcMCFlvdXJDaXR5MR0wGwYDVQQKDBRFeGFt +cGxlLUNlcnRpZmljYXRlczEYMBYGA1UEAwwPbG9jYWxob3N0LmxvY2FsMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwUYH95U11TYvYn9or/cP/oPtiJJm +l1aH5J2AOfno/QW5FjDKeGUYeXrGTOLeDRiHIAHVtg00nfTtm56du5+I972VbEW6 +ngrQAGyk/GSvOGK1iG4M4eHpCkZHrdkubOWsO5vDj2juTwjE1fyncLmIbyZN5NKJ +6JID8c/ItFXQzXDLaFAQH3+uFFX9iTeT2cGyFEidooKo4oKj3imQRM6YKl4ygEMc +BTN0p7AqtjKSfva5WsMSXdESLsNdStgs5OgfJUE8x4jhcrVrZ2Mnw8Zmfb2pIoNe +6HphVtU96EMDqZmRJ4c9REqS6qZmHKsAPet/jyNNZjUJIptJE6Cl0IihhwIDAQAB +o3AwbjAfBgNVHSMEGDAWgBTCPiGSDMoinNiPFsW4fYZW3XDPKjAJBgNVHRMEAjAA +MAsGA1UdDwQEAwIE8DAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFIni +f5YpRjm9GEjktq2gr/34isB1MA0GCSqGSIb3DQEBCwUAA4IBAQCAv0FdWz+Ktq6+ +I7wOUWx3LHFQSov71ca9HGbD9SMOBbd26MJXnxxjOU62W+Q/Bkx4C3ezzSuLMhZq +Vl3jV2wDkWWCFvR1a2cwmr05B2xs/lqX+eNwrNZNfGxMV4I4dn+bkGe2czRcZaur +et0SylTQvWwrCgewRirS44LcN9EEutu0WzxlUpdty71zlVKdpyE6MrZ10ADcin5B +P/10++aBuQXF1EDFGZ6snexGuSsshAP2tBLww+Z8xPyN3SRMj/Y0z7Rgm4nhEfNP +M1BcUiaT7e8evsWVAyoDI6N1Z4M0p4SXLLeHdhKelbvNdeVZxwDkl8jR+Q5hwt7K +ZurEL1FZ +-----END CERTIFICATE----- diff --git a/frontend/.local/localhost.csr b/frontend/.local/localhost.csr new file mode 100644 index 0000000..87b5f46 --- /dev/null +++ b/frontend/.local/localhost.csr @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICsjCCAZoCAQAwbTELMAkGA1UEBhMCVVMxEjAQBgNVBAgMCVlvdXJTdGF0ZTER +MA8GA1UEBwwIWW91ckNpdHkxHTAbBgNVBAoMFEV4YW1wbGUtQ2VydGlmaWNhdGVz +MRgwFgYDVQQDDA9sb2NhbGhvc3QubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDBRgf3lTXVNi9if2iv9w/+g+2IkmaXVofknYA5+ej9BbkWMMp4 +ZRh5esZM4t4NGIcgAdW2DTSd9O2bnp27n4j3vZVsRbqeCtAAbKT8ZK84YrWIbgzh +4ekKRket2S5s5aw7m8OPaO5PCMTV/KdwuYhvJk3k0onokgPxz8i0VdDNcMtoUBAf +f64UVf2JN5PZwbIUSJ2igqjigqPeKZBEzpgqXjKAQxwFM3SnsCq2MpJ+9rlawxJd +0RIuw11K2Czk6B8lQTzHiOFytWtnYyfDxmZ9vakig17oemFW1T3oQwOpmZEnhz1E +SpLqpmYcqwA963+PI01mNQkim0kToKXQiKGHAgMBAAGgADANBgkqhkiG9w0BAQsF +AAOCAQEAZUPJaPBXjSsbDoWEREiOrbai0sVRa3Qr/IIz3Z/+Nw9m2E+j9O11Vi8G +G3eqkixw/m4StbKZyMg+PAQlLC5NUmgMC5w9lfODq/CSRY8UVUi/EiIAMI4Rh/Dk +rGVsYArCcsmbWUzB5oUw4oUtgv0fWsuM0Thc7GORvtQvq4xzPcpWa1L88kRA08Mv +P8Yyk3txYQlkk3Bd40UL8TN6XiQC2IF1b/kXizqr2HL+1BiqCDUWtdDZmVZTvUXH +chUn1tCEm0iJSJZsJLVa5CDWkZHat2A3+8SaxYRL6QlWbiISOhs7KL6fPeTxc/S8 +OKHnA3FRDzQTp+lpjSJWJEqt5W3i+Q== +-----END CERTIFICATE REQUEST----- diff --git a/frontend/.local/localhost.key b/frontend/.local/localhost.key new file mode 100644 index 0000000..a34ad91 --- /dev/null +++ b/frontend/.local/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDBRgf3lTXVNi9i +f2iv9w/+g+2IkmaXVofknYA5+ej9BbkWMMp4ZRh5esZM4t4NGIcgAdW2DTSd9O2b +np27n4j3vZVsRbqeCtAAbKT8ZK84YrWIbgzh4ekKRket2S5s5aw7m8OPaO5PCMTV +/KdwuYhvJk3k0onokgPxz8i0VdDNcMtoUBAff64UVf2JN5PZwbIUSJ2igqjigqPe +KZBEzpgqXjKAQxwFM3SnsCq2MpJ+9rlawxJd0RIuw11K2Czk6B8lQTzHiOFytWtn +YyfDxmZ9vakig17oemFW1T3oQwOpmZEnhz1ESpLqpmYcqwA963+PI01mNQkim0kT +oKXQiKGHAgMBAAECgf9k4w6I+HUuySeHJ+06PhtsELUINvfCPaxhFaAc2TwWNgGC +XfWjTrZ323ZJaMTsEReFWkdqLDciqKoA8AEVQCQxmY7fVA1mwt/QtzZ1lrq3zx+w +Ci3yrZvCBJzES0kmgXmpo+i0eh8T3waqkcwz1XzON3PP8J1qoOiWoe1Yawd4oMIq +QN0/DE1Zdz23QtWo0OLZw0/tLpB3cTK2XpzpBLh+OLImLmA3mqhbsJKzoVD79MTF +emgjFumsZqf0+jUU6tn2+XP83dxeL4yRQGfI19Qg0g+AF4Ue39/7Zife127h7x8o +iknk0m1WSyXjNNxFk1hceHz0CE1qWh/OxuWK+lkCgYEA9uxcmcSYvqMBQKd/q33r +v9wB5jHOPNGXapawKDTCW6B77bvKiYwP0wGNcIV0gDqFdt1RnS4OC3ERoiuhgZum +C7m2r/WOF+ktpkdMaiOLnrtPcAeZKItALAzTqAsbEDHupuqMvQ9Rlk33K/p+SsMt +YXEU0lbe7nsTVHUwZIEnymMCgYEAyGDOTUTJjWIL6x2n7kIWo+kxvCtL7rSmS6LY +XgyE7YTAbptEPtSG4iZTwp2mV6teeakIJ8he6Qw+DlMI+25tDjODMiX2CTbDUPD7 +Koz/7rvX1KvqLGDocik3dzNEgimHamn9edR4he+yOuVNQTeQKyT2kiTS13PDUSyx +c511A40CgYALcE+p3+2jhzGbqSbE7wRo0uxTmaMQNjVpfqmScz89t2WGUdjEHnTt +NXkz6TrIZe2g4AfT3DpSVV6XwKWut2Mtz+CiriFzAh5XQZ5MnHDvXYYEjpWNx0Wl +xMJZWopaabXAjcUPTRJegCYfJ2+KTxhfFzn7tIKHUbRHiVZAkasiOQKBgQC0yGJY +5E7T+Dm4jrEMDSRQDfyJQKbpXqMKaom7kJ/9i1/yarNP3H81Wk8HryR8xGgga5io +C+dVa24xjkj0T9Skk2UywX0RIw9k34Y8BaqCwYcgoVpNX8FX/kfEi8SrirxaDfZb +DI8QiAnNiUQCKxcvVdIAWKjBKfh2LFSNcuo3oQKBgDuFJRHTBOkSTFIImVoxAV8i +duZrLuhgrzyLAuH5kMNGW8hm3dHYRMFm86nrWyMQgY52dar0TNzbPRvy2qU/2L8g +LxFrVO3pw5uZbYrcuhWAqLJCkJTDqawHvLGy5L5YJHewxBUszNHJIaluh0MvlJev +c+jFMCSN9BLo6Fy50vM8 +-----END PRIVATE KEY----- diff --git a/frontend/.local/make_ca.sh b/frontend/.local/make_ca.sh new file mode 100755 index 0000000..f161de1 --- /dev/null +++ b/frontend/.local/make_ca.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +cd $(dirname $0) + +rf -rf RootCA.* + +openssl req -x509 -nodes -new -sha256 -days 1024 -newkey rsa:2048 -keyout RootCA.key -out RootCA.pem -subj "/C=US/CN=Example-Root-CA" +openssl x509 -outform pem -in RootCA.pem -out RootCA.crt \ No newline at end of file diff --git a/frontend/.local/make_localhost.sh b/frontend/.local/make_localhost.sh new file mode 100755 index 0000000..1accc99 --- /dev/null +++ b/frontend/.local/make_localhost.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +cd $(dirname $0) + +rf -rf localhost.* + +openssl req -new -nodes -newkey rsa:2048 -keyout localhost.key -out localhost.csr -subj "/C=US/ST=YourState/L=YourCity/O=Example-Certificates/CN=localhost.local" +openssl x509 -req -sha256 -days 1024 -in localhost.csr -CA RootCA.pem -CAkey RootCA.key -CAcreateserial -extfile domains.ext -out localhost.crt + +cat localhost.crt RootCA.crt > fullchain.pem +cat localhost.key > privkey.pem \ No newline at end of file diff --git a/frontend/.local/privkey.pem b/frontend/.local/privkey.pem new file mode 100644 index 0000000..a34ad91 --- /dev/null +++ b/frontend/.local/privkey.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDBRgf3lTXVNi9i +f2iv9w/+g+2IkmaXVofknYA5+ej9BbkWMMp4ZRh5esZM4t4NGIcgAdW2DTSd9O2b +np27n4j3vZVsRbqeCtAAbKT8ZK84YrWIbgzh4ekKRket2S5s5aw7m8OPaO5PCMTV +/KdwuYhvJk3k0onokgPxz8i0VdDNcMtoUBAff64UVf2JN5PZwbIUSJ2igqjigqPe +KZBEzpgqXjKAQxwFM3SnsCq2MpJ+9rlawxJd0RIuw11K2Czk6B8lQTzHiOFytWtn +YyfDxmZ9vakig17oemFW1T3oQwOpmZEnhz1ESpLqpmYcqwA963+PI01mNQkim0kT +oKXQiKGHAgMBAAECgf9k4w6I+HUuySeHJ+06PhtsELUINvfCPaxhFaAc2TwWNgGC +XfWjTrZ323ZJaMTsEReFWkdqLDciqKoA8AEVQCQxmY7fVA1mwt/QtzZ1lrq3zx+w +Ci3yrZvCBJzES0kmgXmpo+i0eh8T3waqkcwz1XzON3PP8J1qoOiWoe1Yawd4oMIq +QN0/DE1Zdz23QtWo0OLZw0/tLpB3cTK2XpzpBLh+OLImLmA3mqhbsJKzoVD79MTF +emgjFumsZqf0+jUU6tn2+XP83dxeL4yRQGfI19Qg0g+AF4Ue39/7Zife127h7x8o +iknk0m1WSyXjNNxFk1hceHz0CE1qWh/OxuWK+lkCgYEA9uxcmcSYvqMBQKd/q33r +v9wB5jHOPNGXapawKDTCW6B77bvKiYwP0wGNcIV0gDqFdt1RnS4OC3ERoiuhgZum +C7m2r/WOF+ktpkdMaiOLnrtPcAeZKItALAzTqAsbEDHupuqMvQ9Rlk33K/p+SsMt +YXEU0lbe7nsTVHUwZIEnymMCgYEAyGDOTUTJjWIL6x2n7kIWo+kxvCtL7rSmS6LY +XgyE7YTAbptEPtSG4iZTwp2mV6teeakIJ8he6Qw+DlMI+25tDjODMiX2CTbDUPD7 +Koz/7rvX1KvqLGDocik3dzNEgimHamn9edR4he+yOuVNQTeQKyT2kiTS13PDUSyx +c511A40CgYALcE+p3+2jhzGbqSbE7wRo0uxTmaMQNjVpfqmScz89t2WGUdjEHnTt +NXkz6TrIZe2g4AfT3DpSVV6XwKWut2Mtz+CiriFzAh5XQZ5MnHDvXYYEjpWNx0Wl +xMJZWopaabXAjcUPTRJegCYfJ2+KTxhfFzn7tIKHUbRHiVZAkasiOQKBgQC0yGJY +5E7T+Dm4jrEMDSRQDfyJQKbpXqMKaom7kJ/9i1/yarNP3H81Wk8HryR8xGgga5io +C+dVa24xjkj0T9Skk2UywX0RIw9k34Y8BaqCwYcgoVpNX8FX/kfEi8SrirxaDfZb +DI8QiAnNiUQCKxcvVdIAWKjBKfh2LFSNcuo3oQKBgDuFJRHTBOkSTFIImVoxAV8i +duZrLuhgrzyLAuH5kMNGW8hm3dHYRMFm86nrWyMQgY52dar0TNzbPRvy2qU/2L8g +LxFrVO3pw5uZbYrcuhWAqLJCkJTDqawHvLGy5L5YJHewxBUszNHJIaluh0MvlJev +c+jFMCSN9BLo6Fy50vM8 +-----END PRIVATE KEY----- diff --git a/deploy/prod/Dockerfile b/frontend/Dockerfile similarity index 100% rename from deploy/prod/Dockerfile rename to frontend/Dockerfile diff --git a/frontend/extras b/frontend/extras index 21bf28a..98d7936 160000 --- a/frontend/extras +++ b/frontend/extras @@ -1 +1 @@ -Subproject commit 21bf28a8b6293872f5f7a42c9ca0bc12ede5aaae +Subproject commit 98d7936a50a148e0c2c718b3d7b8662ffcb3fde8 diff --git a/frontend/fullchain.pem b/frontend/fullchain.pem new file mode 100644 index 0000000..f733cbd --- /dev/null +++ b/frontend/fullchain.pem @@ -0,0 +1,87 @@ +-----BEGIN CERTIFICATE----- +MIIEXTCCA0WgAwIBAgISBEHk0Sh8UrMfT+VPkRhr83mfMA0GCSqGSIb3DQEBCwUA +MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD +EwJSMzAeFw0yMzA2MDIxNzI1MDVaFw0yMzA4MzExNzI1MDRaMBsxGTAXBgNVBAMT +EHRvb2xzaGVkLmozZDEuZGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARYeMpT +k1DaC8cigL3DivanGrLQahYBEDm5B26VaS3gUmq9T0RNkEUxJIPnZBwdF8p7xAEB +hlTXwgy3eBLAp8lAo4ICTTCCAkkwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQG +CCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTRtqOn +Qht7rD+2IEGzuYt8frTgVjAfBgNVHSMEGDAWgBQULrMXt1hWy65QCUDmH6+dixTC +xjBVBggrBgEFBQcBAQRJMEcwIQYIKwYBBQUHMAGGFWh0dHA6Ly9yMy5vLmxlbmNy +Lm9yZzAiBggrBgEFBQcwAoYWaHR0cDovL3IzLmkubGVuY3Iub3JnLzAbBgNVHREE +FDASghB0b29sc2hlZC5qM2QxLmRlMEwGA1UdIARFMEMwCAYGZ4EMAQIBMDcGCysG +AQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQu +b3JnMIIBBgYKKwYBBAHWeQIEAgSB9wSB9ADyAHcAtz77JN+cTbp18jnFulj0bF38 +Qs96nzXEnh0JgSXttJkAAAGIfVskxgAABAMASDBGAiEA+D8rCaCpttJm7w0M4N5N +3cmJSfPNmh/t2ojaDB0iSe0CIQCS2XkwJzUrDZ35fIJ9evwJduk/K2I/tmWs4Uk5 +vnPSNQB3AK33vvp8/xDIi509nB4+GGq0Zyldz7EMJMqFhjTr3IKKAAABiH1bJOoA +AAQDAEgwRgIhAPHQQwLf5xSi1VH6BeOpiUKyTMawd36FFU8eCIdB43q6AiEA0KDD +yRssPcmGnyWDGq9Of3mpKjCChFrnxzpeDXCTlsswDQYJKoZIhvcNAQELBQADggEB +AKo8APReSKNTydks9yqASKhUjuLfXS+mpFQSl2tbU8ER6eIiYHx8o+n2QCdT7h91 +ZLkGx8ZAmWBvVwXC3QPH5W08ilogi4EU/+HGffkditG5K6/Qn2bzjqnmFIyYqgdT +RVaRcxqS9byAGEw3oU5FSCIOuFSBeOHeTwaj+lSVMZTv6LmoovOpCo8sA5xZ6K6H +XVwNXIwunssaR4MrnWupB/5N+T7zkhanky4GgiLRuTm+mDbK+OIDx47Hv9jTe+tm +s4aixD0eWhzAaiA7HuHJI3Xi64YjK7eNlrwE0ZKdgy8KveDwUBiVcVtz7LR+0v1l +P27Z/OkZlA+42LvIJdISMl4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw +WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg +RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP +R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx +sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm +NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg +Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG +/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB +Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA +FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw +AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw +Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB +gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W +PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl +ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz +CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm +lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4 +avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2 +yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O +yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids +hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+ +HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv +MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX +nLRbwHOoq7hHwg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFYDCCBEigAwIBAgIQQAF3ITfU6UK47naqPGQKtzANBgkqhkiG9w0BAQsFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTIxMDEyMDE5MTQwM1oXDTI0MDkzMDE4MTQwM1ow +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCt6CRz9BQ385ueK1coHIe+3LffOJCMbjzmV6B493XC +ov71am72AE8o295ohmxEk7axY/0UEmu/H9LqMZshftEzPLpI9d1537O4/xLxIZpL +wYqGcWlKZmZsj348cL+tKSIG8+TA5oCu4kuPt5l+lAOf00eXfJlII1PoOK5PCm+D +LtFJV4yAdLbaL9A4jXsDcCEbdfIwPPqPrt3aY6vrFk/CjhFLfs8L6P+1dy70sntK +4EwSJQxwjQMpoOFTJOwT2e4ZvxCzSow/iaNhUd6shweU9GNx7C7ib1uYgeGJXDR5 +bHbvO5BieebbpJovJsXQEOEO3tkQjhb7t/eo98flAgeYjzYIlefiN5YNNnWe+w5y +sR2bvAP5SQXYgd0FtCrWQemsAXaVCg/Y39W9Eh81LygXbNKYwagJZHduRze6zqxZ +Xmidf3LWicUGQSk+WT7dJvUkyRGnWqNMQB9GoZm1pzpRboY7nn1ypxIFeFntPlF4 +FQsDj43QLwWyPntKHEtzBRL8xurgUBN8Q5N0s8p0544fAQjQMNRbcTa0B7rBMDBc +SLeCO5imfWCKoqMpgsy6vYMEG6KDA0Gh1gXxG8K28Kh8hjtGqEgqiNx2mna/H2ql +PRmP6zjzZN7IKw0KKP/32+IVQtQi0Cdd4Xn+GOdwiK1O5tmLOsbdJ1Fu/7xk9TND +TwIDAQABo4IBRjCCAUIwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +SwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5pZGVudHJ1 +c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTEp7Gkeyxx ++tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEB +ATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQu +b3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0LmNvbS9E +U1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFHm0WeZ7tuXkAXOACIjIGlj26Ztu +MA0GCSqGSIb3DQEBCwUAA4IBAQAKcwBslm7/DlLQrt2M51oGrS+o44+/yQoDFVDC +5WxCu2+b9LRPwkSICHXM6webFGJueN7sJ7o5XPWioW5WlHAQU7G75K/QosMrAdSW +9MUgNTP52GE24HGNtLi1qoJFlcDyqSMo59ahy2cI2qBDLKobkx/J3vWraV0T9VuG +WCLKTVXkcGdtwlfFRjlBz4pYg1htmf5X6DYO8A4jqv2Il9DjXA6USbW1FzXSLr9O +he8Y4IWS6wY7bCkjCWDcRQJMEhg76fsO3txE+FiYruq9RUWhiF1myv4Q6W+CyBFC +Dfvp7OOGAN6dEOM4+qR9sdjoSYKEBpsr6GtPAQw4dy753ec5 +-----END CERTIFICATE----- diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..d6ee127 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,14 @@ +server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + } + + location /api { + proxy_pass http://django:8000; + } + +} \ No newline at end of file diff --git a/frontend/src/components/AuthenticatedAvatar.vue b/frontend/node_modules/.forgit similarity index 100% rename from frontend/src/components/AuthenticatedAvatar.vue rename to frontend/node_modules/.forgit diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7de8ccb..e726ed1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,650 +1,973 @@ { "name": "frontend", "version": "0.0.0", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==" - }, - "@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==" - }, - "@babel/parser": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", - "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", - "requires": { - "@babel/types": "^7.25.6" + "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" } }, - "@babel/types": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", - "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", - "requires": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "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" } }, - "@esbuild/android-arm": { + "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 + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/android-arm64": { + "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 + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/android-x64": { + "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 + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/darwin-arm64": { + "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 + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/darwin-x64": { + "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 + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/freebsd-arm64": { + "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 + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/freebsd-x64": { + "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 + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-arm": { + "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 + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-arm64": { + "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 + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-ia32": { + "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 + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-loong64": { + "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 + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-mips64el": { + "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 + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-ppc64": { + "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 + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-riscv64": { + "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 + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-s390x": { + "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 + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/linux-x64": { + "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 + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/netbsd-x64": { + "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 + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/openbsd-x64": { + "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 + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/sunos-x64": { + "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 + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/win32-arm64": { + "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 + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/win32-ia32": { + "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 + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } }, - "@esbuild/win32-x64": { + "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 + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } }, - "@isaacs/cliui": { + "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, - "requires": { + "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" } }, - "@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + "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==" }, - "@leichtgewicht/base64-codec": { + "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==" }, - "@leichtgewicht/dns-packet": { + "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==", - "requires": { + "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" } }, - "@leichtgewicht/dns-socket": { + "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==", - "requires": { + "dependencies": { "@leichtgewicht/dns-packet": "^6.0.0" + }, + "engines": { + "node": ">=6" } }, - "@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==" + "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==" }, - "@one-ini/wasm": { + "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 }, - "@pkgjs/parseargs": { + "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 + "optional": true, + "engines": { + "node": ">=14" + } }, - "@tootallnate/once": { + "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 }, - "@types/chai": { - "version": "4.3.19", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.19.tgz", - "integrity": "sha512-2hHHvQBVE2FiSK4eN0Br6snX9MtolHaTo/batnLjlGRhoQzlCL61iVpxoqO7SfFyOw+P/pwv+0zNHzKoGWz9Cw==", - "dev": true - }, - "@types/chai-subset": { + "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, - "requires": { + "dependencies": { "@types/chai": "*" } }, - "@types/node": { - "version": "22.5.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", - "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "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, - "requires": { - "undici-types": "~6.19.2" + "dependencies": { + "undici-types": "~5.26.4" } }, - "@vitejs/plugin-vue": { + "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 + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.2.25" + } }, - "@vitest/expect": { + "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, - "requires": { + "dependencies": { "@vitest/spy": "0.31.4", "@vitest/utils": "0.31.4", "chai": "^4.3.7" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "@vitest/runner": { + "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, - "requires": { + "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" } }, - "@vitest/snapshot": { + "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, - "requires": { + "dependencies": { "magic-string": "^0.30.0", "pathe": "^1.1.0", "pretty-format": "^27.5.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "@vitest/spy": { + "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, - "requires": { + "dependencies": { "tinyspy": "^2.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "@vitest/utils": { + "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, - "requires": { + "dependencies": { "concordance": "^5.0.4", "loupe": "^2.3.6", "pretty-format": "^27.5.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "@vue/compiler-core": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.3.tgz", - "integrity": "sha512-adAfy9boPkP233NTyvLbGEqVuIfK/R0ZsBsIOW4BZNfb4BRpRW41Do1u+ozJpsb+mdoy80O20IzAsHaihRb5qA==", - "requires": { - "@babel/parser": "^7.25.3", - "@vue/shared": "3.5.3", + "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.2.0" + "source-map-js": "^1.0.2" } }, - "@vue/compiler-dom": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.3.tgz", - "integrity": "sha512-wnzFArg9zpvk/811CDOZOadJRugf1Bgl/TQ3RfV4nKfSPok4hi0w10ziYUQR6LnnBAUlEXYLUfZ71Oj9ds/+QA==", - "requires": { - "@vue/compiler-core": "3.5.3", - "@vue/shared": "3.5.3" + "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" } }, - "@vue/compiler-sfc": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.3.tgz", - "integrity": "sha512-P3uATLny2tfyvMB04OQFe7Sczteno7SLFxwrOA/dw01pBWQHB5HL15a8PosoNX2aG/EAMGqnXTu+1LnmzFhpTQ==", - "requires": { - "@babel/parser": "^7.25.3", - "@vue/compiler-core": "3.5.3", - "@vue/compiler-dom": "3.5.3", - "@vue/compiler-ssr": "3.5.3", - "@vue/shared": "3.5.3", + "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.11", - "postcss": "^8.4.44", - "source-map-js": "^1.2.0" + "magic-string": "^0.30.7", + "postcss": "^8.4.35", + "source-map-js": "^1.0.2" } }, - "@vue/compiler-ssr": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.3.tgz", - "integrity": "sha512-F/5f+r2WzL/2YAPl7UlKcJWHrvoZN8XwEBLnT7S4BXwncH25iDOabhO2M2DWioyTguJAGavDOawejkFXj8EM1w==", - "requires": { - "@vue/compiler-dom": "3.5.3", - "@vue/shared": "3.5.3" + "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" } }, - "@vue/devtools-api": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", - "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + "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==" }, - "@vue/reactivity": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.3.tgz", - "integrity": "sha512-2w61UnRWTP7+rj1H/j6FH706gRBHdFVpIqEkSDAyIpafBXYH8xt4gttstbbCWdU3OlcSWO8/3mbKl/93/HSMpw==", - "requires": { - "@vue/shared": "3.5.3" + "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" } }, - "@vue/runtime-core": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.3.tgz", - "integrity": "sha512-5b2AQw5OZlmCzSsSBWYoZOsy75N4UdMWenTfDdI5bAzXnuVR7iR8Q4AOzQm2OGoA41xjk53VQKrqQhOz2ktWaw==", - "requires": { - "@vue/reactivity": "3.5.3", - "@vue/shared": "3.5.3" + "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" } }, - "@vue/runtime-dom": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.3.tgz", - "integrity": "sha512-wPR1DEGc3XnQ7yHbmkTt3GoY0cEnVGQnARRdAkDzZ8MbUKEs26gogCQo6AOvvgahfjIcnvWJzkZArQ1fmWjcSg==", - "requires": { - "@vue/reactivity": "3.5.3", - "@vue/runtime-core": "3.5.3", - "@vue/shared": "3.5.3", + "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" } }, - "@vue/server-renderer": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.3.tgz", - "integrity": "sha512-28volmaZVG2PGO3V3+gBPKoSHvLlE8FGfG/GKXKkjjfxLuj/50B/0OQGakM/g6ehQeqCrZYM4eHC4Ks48eig1Q==", - "requires": { - "@vue/compiler-ssr": "3.5.3", - "@vue/shared": "3.5.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" } }, - "@vue/shared": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.3.tgz", - "integrity": "sha512-Jp2v8nylKBT+PlOUjun2Wp/f++TfJVFjshLzNtJDdmFJabJa7noGMncqXRM1vXGX+Yo2V7WykQFNxusSim8SCA==" + "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==" }, - "@vue/test-utils": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", - "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", + "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, - "requires": { + "dependencies": { "js-beautify": "^1.14.9", "vue-component-type-helpers": "^2.0.0" } }, - "abab": { + "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 }, - "abbrev": { + "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 - }, - "acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "dev": true - }, - "acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", "dev": true, - "requires": { - "acorn": "^8.11.0" + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "agent-base": { + "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, - "requires": { + "dependencies": { "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" } }, - "ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true + "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" + } }, - "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 + "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" + } }, - "anymatch": { + "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, - "requires": { + "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" } }, - "assertion-error": { + "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 + "dev": true, + "engines": { + "node": "*" + } }, - "asynckit": { + "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 }, - "balanced-match": { + "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 }, - "binary-extensions": { + "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 + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "blueimp-md5": { + "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 }, - "bootstrap": { + "node_modules/bootstrap": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", - "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==" + "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" + } }, - "bootstrap-icons-vue": { + "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==" }, - "brace-expansion": { + "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, - "requires": { + "dependencies": { "balanced-match": "^1.0.0" } }, - "braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "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, - "requires": { - "fill-range": "^7.1.1" + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" } }, - "bytes.js": { + "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==" }, - "cac": { + "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 - }, - "chai": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", - "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, - "requires": { + "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.1.0" + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" } }, - "check-error": { + "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, - "requires": { + "dependencies": { "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" } }, - "chokidar": { + "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, - "requires": { + "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", - "fsevents": "~2.3.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" } }, - "color-convert": { + "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, - "requires": { + "dependencies": { "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "color-name": { + "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 }, - "combined-stream": { + "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, - "requires": { + "dependencies": { "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "commander": { + "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 + "dev": true, + "engines": { + "node": ">=14" + } }, - "concordance": { + "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, - "requires": { + "dependencies": { "date-time": "^3.1.0", "esutils": "^2.0.3", "fast-diff": "^1.2.0", @@ -653,155 +976,204 @@ "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" } }, - "confbox": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", - "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", - "dev": true - }, - "config-chain": { + "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, - "requires": { + "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, - "cross-spawn": { + "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, - "requires": { + "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" } }, - "cssstyle": { + "node_modules/cssstyle": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", "dev": true, - "requires": { + "dependencies": { "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" } }, - "csstype": { + "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, - "data-urls": { + "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, - "requires": { + "dependencies": { "abab": "^2.0.6", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" } }, - "date-time": { + "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, - "requires": { + "dependencies": { "time-zone": "^1.0.0" + }, + "engines": { + "node": ">=6" } }, - "debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "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, - "requires": { - "ms": "^2.1.3" + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "decimal.js": { + "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 }, - "deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "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, - "requires": { + "dependencies": { "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" } }, - "delayed-stream": { + "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 + "dev": true, + "engines": { + "node": ">=0.4.0" + } }, - "dns-query": { + "node_modules/dns-query": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/dns-query/-/dns-query-0.11.2.tgz", "integrity": "sha512-zF8qxQpqCB467o4A63DLpQClo77H642JEKMx0Ra9GFww7Rx0234Fo8NoG0LBoSBZxamWkXfLxhzDG19bTBHvXQ==", - "requires": { + "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" } }, - "domexception": { + "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, - "requires": { + "dependencies": { "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, - "eastasianwidth": { + "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 }, - "editorconfig": { + "node_modules/editorconfig": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", "dev": true, - "requires": { + "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" } }, - "emoji-regex": { + "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 }, - "entities": { + "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, - "esbuild": { + "node_modules/esbuild": { "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "dev": true, - "requires": { + "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", @@ -826,253 +1198,328 @@ "@esbuild/win32-x64": "0.18.20" } }, - "estree-walker": { + "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==" }, - "esutils": { + "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 + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "extras": { - "version": "file:extras" - }, - "fast-diff": { + "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 }, - "fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "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, - "requires": { + "dependencies": { "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "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, - "requires": { + "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "form-data": { + "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, - "requires": { + "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, - "fsevents": { + "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, - "optional": true + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, - "get-func-name": { + "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 - }, - "glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "dependencies": { - "minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } + "engines": { + "node": "*" } }, - "glob-parent": { + "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, - "requires": { + "dependencies": { "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, - "html-encoding-sniffer": { + "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, - "requires": { + "dependencies": { "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" } }, - "http-proxy-agent": { + "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, - "requires": { + "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" + }, + "engines": { + "node": ">= 6" } }, - "https-proxy-agent": { + "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, - "requires": { + "dependencies": { "agent-base": "6", "debug": "4" + }, + "engines": { + "node": ">= 6" } }, - "iconv-lite": { + "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, - "requires": { + "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "immutable": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", - "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "node_modules/immutable": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==", "dev": true }, - "ini": { + "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 }, - "is-binary-path": { + "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, - "requires": { + "dependencies": { "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, - "is-extglob": { + "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 + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "is-fullwidth-code-point": { + "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 + "dev": true, + "engines": { + "node": ">=8" + } }, - "is-glob": { + "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, - "requires": { + "dependencies": { "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "is-number": { + "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 + "dev": true, + "engines": { + "node": ">=0.12.0" + } }, - "is-potential-custom-element-name": { + "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 }, - "isexe": { + "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "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, - "requires": { - "@isaacs/cliui": "^8.0.2", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, - "jquery": { + "node_modules/jquery": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "peer": true }, - "js-beautify": { + "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, - "requires": { + "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" } }, - "js-cookie": { + "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 + "dev": true, + "engines": { + "node": ">=14" + } }, - "js-nacl": { + "node_modules/js-nacl": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/js-nacl/-/js-nacl-1.4.0.tgz", - "integrity": "sha512-HgYLcutGbMYBJrwgVICiHliuw1OJLy2U3tIuK6a1rZ06KC84TPl81WG1hcBRrBCiIIuBe3PSo9G4IZOMGdSg3Q==" + "integrity": "sha512-HgYLcutGbMYBJrwgVICiHliuw1OJLy2U3tIuK6a1rZ06KC84TPl81WG1hcBRrBCiIIuBe3PSo9G4IZOMGdSg3Q==", + "engines": { + "node": "*" + } }, - "js-string-escape": { + "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 + "dev": true, + "engines": { + "node": ">= 0.8" + } }, - "jsdom": { + "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, - "requires": { + "dependencies": { "abab": "^2.0.6", "cssstyle": "^3.0.0", "data-urls": "^4.0.0", @@ -1096,612 +1543,882 @@ "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 + } } }, - "local-pkg": { + "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 + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } }, - "lodash": { + "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "loupe": { + "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, - "requires": { + "dependencies": { "get-func-name": "^2.0.1" } }, - "lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true - }, - "magic-string": { - "version": "0.30.11", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", - "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", - "requires": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "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" } }, - "md5-hex": { + "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, - "requires": { + "dependencies": { "blueimp-md5": "^2.10.0" + }, + "engines": { + "node": ">=8" } }, - "mime-db": { + "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 + "dev": true, + "engines": { + "node": ">= 0.6" + } }, - "mime-types": { + "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, - "requires": { + "dependencies": { "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, - "minimatch": { + "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, - "requires": { + "dependencies": { "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true - }, - "mlly": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", - "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", "dev": true, - "requires": { + "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.1.1", - "ufo": "^1.5.3" + "pkg-types": "^1.0.3", + "ufo": "^1.3.2" } }, - "moment": { + "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==" - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" - }, - "nopt": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", - "dev": true, - "requires": { - "abbrev": "^2.0.0" + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" } }, - "normalize-path": { + "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 }, - "nwsapi": { - "version": "2.2.12", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", - "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", - "dev": true - }, - "p-limit": { + "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, - "requires": { + "dependencies": { "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true - }, - "parse5": { + "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", "dev": true, - "requires": { + "dependencies": { "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "path-key": { + "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 - }, - "path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, - "requires": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "engines": { + "node": ">=8" } }, - "pathe": { + "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 }, - "pathval": { + "node_modules/pathval": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true + "dev": true, + "engines": { + "node": "*" + } }, - "picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, - "picomatch": { + "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pkg-types": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.0.tgz", - "integrity": "sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA==", "dev": true, - "requires": { - "confbox": "^0.1.7", - "mlly": "^1.7.1", - "pathe": "^1.1.2" + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "popper.js": { + "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==" - }, - "postcss": { - "version": "8.4.45", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz", - "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==", - "requires": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "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" } }, - "pretty-format": { + "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, - "requires": { + "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" }, - "dependencies": { - "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 - }, - "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.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "proto-list": { + "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 }, - "psl": { + "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 }, - "punycode": { + "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 + "dev": true, + "engines": { + "node": ">=6" + } }, - "querystringify": { + "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 }, - "react-is": { + "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 }, - "readdirp": { + "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, - "requires": { + "dependencies": { "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" } }, - "requires-port": { + "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 }, - "rollup": { + "node_modules/rollup": { "version": "3.29.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", "dev": true, - "requires": { + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { "fsevents": "~2.3.2" } }, - "rrweb-cssom": { + "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 }, - "safer-buffer": { + "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 }, - "sass": { - "version": "1.78.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.78.0.tgz", - "integrity": "sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==", + "node_modules/sass": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.72.0.tgz", + "integrity": "sha512-Gpczt3WA56Ly0Mn8Sl21Vj94s1axi9hDIzDFn9Ph9x3C3p4nNyvsqJoQyVXKou6cBlfFWEgRW4rT8Tb4i3XnVA==", "dev": true, - "requires": { + "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" } }, - "saxes": { + "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, - "requires": { + "dependencies": { "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" } }, - "semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true + "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" + } }, - "shebang-command": { + "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, - "requires": { + "dependencies": { "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "shebang-regex": { + "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 + "dev": true, + "engines": { + "node": ">=8" + } }, - "siginfo": { + "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 }, - "signal-exit": { + "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 + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + "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" + } }, - "stackback": { + "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 }, - "std-env": { + "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 }, - "string-width": { + "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, - "requires": { + "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" } }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", + "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, - "requires": { + "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" }, - "dependencies": { - "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 - }, - "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 - }, - "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, - "requires": { - "ansi-regex": "^5.0.1" - } - } + "engines": { + "node": ">=8" } }, - "strip-ansi": { + "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, - "requires": { + "dependencies": { "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.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, - "requires": { + "dependencies": { "ansi-regex": "^5.0.1" }, - "dependencies": { - "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" } }, - "strip-literal": { + "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, - "requires": { + "dependencies": { "acorn": "^8.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "symbol-tree": { + "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 }, - "time-zone": { + "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 }, - "tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true - }, - "tinypool": { + "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 + "dev": true, + "engines": { + "node": ">=14.0.0" + } }, - "tinyspy": { + "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 + "dev": true, + "engines": { + "node": ">=14.0.0" + } }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" - }, - "to-regex-range": { + "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, - "requires": { + "dependencies": { "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "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, - "requires": { + "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" } }, - "tr46": { + "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, - "requires": { + "dependencies": { "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" } }, - "type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "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 }, - "ufo": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", - "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "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 }, - "undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true - }, - "universalify": { + "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 + "dev": true, + "engines": { + "node": ">= 4.0.0" + } }, - "url-parse": { + "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, - "requires": { + "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, - "utf8-bytes": { + "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==" }, - "utf8-codec": { + "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==" }, - "utf8-length": { + "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==" }, - "utf8-string-bytes": { + "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==" }, - "vite": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", - "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "node_modules/vite": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "dev": true, - "requires": { + "dependencies": { "esbuild": "^0.18.10", - "fsevents": "~2.3.2", "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 + } } }, - "vite-node": { + "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, - "requires": { + "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" } }, - "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, - "requires": { + "dependencies": { "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", "@types/node": "*", @@ -1727,200 +2444,364 @@ "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 + } } }, - "vue": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.3.tgz", - "integrity": "sha512-xvRbd0HpuLovYbOHXRHlSBsSvmUJbo0pzbkKTApWnQGf3/cu5Z39mQeA5cZdLRVIoNf3zI6MSoOgHUT5i2jO+Q==", - "requires": { - "@vue/compiler-dom": "3.5.3", - "@vue/compiler-sfc": "3.5.3", - "@vue/runtime-dom": "3.5.3", - "@vue/server-renderer": "3.5.3", - "@vue/shared": "3.5.3" + "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 + } } }, - "vue-component-type-helpers": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.1.6.tgz", - "integrity": "sha512-ng11B8B/ZADUMMOsRbqv0arc442q7lifSubD0v8oDXIFoMg/mXwAPUunrroIDkY+mcD0dHKccdaznSVp8EoX3w==", + "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 }, - "vue-multiselect": { + "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==" - }, - "vue-router": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.3.tgz", - "integrity": "sha512-sv6wmNKx2j3aqJQDMxLFzs/u/mjA9Z5LCgy6BE0f7yFWMjrPLnS/sPNn8ARY/FXw6byV18EFutn5lTO6+UsV5A==", - "requires": { - "@vue/devtools-api": "^6.6.3" + "integrity": "sha512-nGEppmzhQQT2iDz4cl+ZCX3BpeNhygK50zWFTIRS+r7K7i61uWXJWSioMuf+V/161EPQjexI8NaEBdUlF3dp+g==", + "engines": { + "node": ">= 4.0.0", + "npm": ">= 3.0.0" } }, - "vuex": { + "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==", - "requires": { + "dependencies": { "@vue/devtools-api": "^6.0.0-beta.11" + }, + "peerDependencies": { + "vue": "^3.2.0" } }, - "w3c-xmlserializer": { + "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, - "requires": { + "dependencies": { "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" } }, - "webidl-conversions": { + "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 + "dev": true, + "engines": { + "node": ">=12" + } }, - "well-known-symbols": { + "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 + "dev": true, + "engines": { + "node": ">=6" + } }, - "whatwg-encoding": { + "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, - "requires": { + "dependencies": { "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" } }, - "whatwg-mimetype": { + "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 + "dev": true, + "engines": { + "node": ">=12" + } }, - "whatwg-url": { + "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, - "requires": { + "dependencies": { "tr46": "^4.1.1", "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" } }, - "which": { + "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, - "requires": { + "dependencies": { "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, - "why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "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, - "requires": { + "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" } }, - "wrap-ansi": { + "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, - "requires": { + "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" } }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", + "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, - "requires": { + "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": { - "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 + "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 }, - "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, - "requires": { - "color-convert": "^2.0.1" - } - }, - "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 - }, - "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, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "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, - "requires": { - "ansi-regex": "^5.0.1" - } + "utf-8-validate": { + "optional": true } } }, - "ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true - }, - "xml-name-validator": { + "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 + "dev": true, + "engines": { + "node": ">=12" + } }, - "xmlchars": { + "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 }, - "yocto-queue": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", - "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "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 index bff1287..fea4d8a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,10 +16,7 @@ "vue": "^3.2.47", "vue-multiselect": "^2.1.7", "vue-router": "^4.1.6", - "vuex": "^4.1.0", - "extras": "file:./extras", - "jquery": "^3.6.0", - "popper.js": "^1.16.1" + "vuex": "^4.1.0" }, "devDependencies": { "@vitejs/plugin-vue": "^4.0.0", diff --git a/frontend/privkey.pem b/frontend/privkey.pem new file mode 100644 index 0000000..96b7dda --- /dev/null +++ b/frontend/privkey.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgD6EmCAUWob1zUw4Q +F+Pf9cSOmSCTODe6u+Gst177IoihRANCAARYeMpTk1DaC8cigL3DivanGrLQahYB +EDm5B26VaS3gUmq9T0RNkEUxJIPnZBwdF8p7xAEBhlTXwgy3eBLAp8lA +-----END PRIVATE KEY----- diff --git a/frontend/src/App.vue b/frontend/src/App.vue index a7280b7..d96b367 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -3,13 +3,23 @@ diff --git a/frontend/src/assets/fonts/Inter-Black.woff b/frontend/src/assets/fonts/Inter-Black.woff new file mode 100644 index 0000000..1b7a081 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-Black.woff differ diff --git a/frontend/src/assets/fonts/Inter-Black.woff2 b/frontend/src/assets/fonts/Inter-Black.woff2 new file mode 100644 index 0000000..1db877f Binary files /dev/null and b/frontend/src/assets/fonts/Inter-Black.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-BlackItalic.woff b/frontend/src/assets/fonts/Inter-BlackItalic.woff new file mode 100644 index 0000000..98f5391 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-BlackItalic.woff differ diff --git a/frontend/src/assets/fonts/Inter-BlackItalic.woff2 b/frontend/src/assets/fonts/Inter-BlackItalic.woff2 new file mode 100644 index 0000000..831279a Binary files /dev/null and b/frontend/src/assets/fonts/Inter-BlackItalic.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-Bold.woff b/frontend/src/assets/fonts/Inter-Bold.woff new file mode 100644 index 0000000..4b05629 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-Bold.woff differ diff --git a/frontend/src/assets/fonts/Inter-Bold.woff2 b/frontend/src/assets/fonts/Inter-Bold.woff2 new file mode 100644 index 0000000..e372976 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-Bold.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-BoldItalic.woff b/frontend/src/assets/fonts/Inter-BoldItalic.woff new file mode 100644 index 0000000..6792723 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-BoldItalic.woff differ diff --git a/frontend/src/assets/fonts/Inter-BoldItalic.woff2 b/frontend/src/assets/fonts/Inter-BoldItalic.woff2 new file mode 100644 index 0000000..c2e8502 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-BoldItalic.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-ExtraBold.woff b/frontend/src/assets/fonts/Inter-ExtraBold.woff new file mode 100644 index 0000000..c2c1355 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-ExtraBold.woff differ diff --git a/frontend/src/assets/fonts/Inter-ExtraBold.woff2 b/frontend/src/assets/fonts/Inter-ExtraBold.woff2 new file mode 100644 index 0000000..125d981 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-ExtraBold.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-ExtraBoldItalic.woff b/frontend/src/assets/fonts/Inter-ExtraBoldItalic.woff new file mode 100644 index 0000000..0cde9dc Binary files /dev/null and b/frontend/src/assets/fonts/Inter-ExtraBoldItalic.woff differ diff --git a/frontend/src/assets/fonts/Inter-ExtraBoldItalic.woff2 b/frontend/src/assets/fonts/Inter-ExtraBoldItalic.woff2 new file mode 100644 index 0000000..b8c0987 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-ExtraBoldItalic.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-ExtraLight-BETA.woff b/frontend/src/assets/fonts/Inter-ExtraLight-BETA.woff new file mode 100644 index 0000000..7d5eb03 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-ExtraLight-BETA.woff differ diff --git a/frontend/src/assets/fonts/Inter-ExtraLight-BETA.woff2 b/frontend/src/assets/fonts/Inter-ExtraLight-BETA.woff2 new file mode 100644 index 0000000..9a40440 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-ExtraLight-BETA.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-ExtraLightItalic-BETA.woff b/frontend/src/assets/fonts/Inter-ExtraLightItalic-BETA.woff new file mode 100644 index 0000000..3e039a7 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-ExtraLightItalic-BETA.woff differ diff --git a/frontend/src/assets/fonts/Inter-ExtraLightItalic-BETA.woff2 b/frontend/src/assets/fonts/Inter-ExtraLightItalic-BETA.woff2 new file mode 100644 index 0000000..0ac33f9 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-ExtraLightItalic-BETA.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-Italic.woff b/frontend/src/assets/fonts/Inter-Italic.woff new file mode 100644 index 0000000..0444e7f Binary files /dev/null and b/frontend/src/assets/fonts/Inter-Italic.woff differ diff --git a/frontend/src/assets/fonts/Inter-Italic.woff2 b/frontend/src/assets/fonts/Inter-Italic.woff2 new file mode 100644 index 0000000..9f134e4 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-Italic.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-Light-BETA.woff b/frontend/src/assets/fonts/Inter-Light-BETA.woff new file mode 100644 index 0000000..8c0556c Binary files /dev/null and b/frontend/src/assets/fonts/Inter-Light-BETA.woff differ diff --git a/frontend/src/assets/fonts/Inter-Light-BETA.woff2 b/frontend/src/assets/fonts/Inter-Light-BETA.woff2 new file mode 100644 index 0000000..e5a1561 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-Light-BETA.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-LightItalic-BETA.woff b/frontend/src/assets/fonts/Inter-LightItalic-BETA.woff new file mode 100644 index 0000000..56f9a3f Binary files /dev/null and b/frontend/src/assets/fonts/Inter-LightItalic-BETA.woff differ diff --git a/frontend/src/assets/fonts/Inter-LightItalic-BETA.woff2 b/frontend/src/assets/fonts/Inter-LightItalic-BETA.woff2 new file mode 100644 index 0000000..2fe1b5f Binary files /dev/null and b/frontend/src/assets/fonts/Inter-LightItalic-BETA.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-Medium.woff b/frontend/src/assets/fonts/Inter-Medium.woff new file mode 100644 index 0000000..b7561d0 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-Medium.woff differ diff --git a/frontend/src/assets/fonts/Inter-Medium.woff2 b/frontend/src/assets/fonts/Inter-Medium.woff2 new file mode 100644 index 0000000..66528cd Binary files /dev/null and b/frontend/src/assets/fonts/Inter-Medium.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-MediumItalic.woff b/frontend/src/assets/fonts/Inter-MediumItalic.woff new file mode 100644 index 0000000..e0ac074 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-MediumItalic.woff differ diff --git a/frontend/src/assets/fonts/Inter-MediumItalic.woff2 b/frontend/src/assets/fonts/Inter-MediumItalic.woff2 new file mode 100644 index 0000000..f39af2e Binary files /dev/null and b/frontend/src/assets/fonts/Inter-MediumItalic.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-Regular.woff b/frontend/src/assets/fonts/Inter-Regular.woff new file mode 100644 index 0000000..ea75a90 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-Regular.woff differ diff --git a/frontend/src/assets/fonts/Inter-Regular.woff2 b/frontend/src/assets/fonts/Inter-Regular.woff2 new file mode 100644 index 0000000..d015841 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-Regular.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-SemiBold.woff b/frontend/src/assets/fonts/Inter-SemiBold.woff new file mode 100644 index 0000000..26b1713 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-SemiBold.woff differ diff --git a/frontend/src/assets/fonts/Inter-SemiBold.woff2 b/frontend/src/assets/fonts/Inter-SemiBold.woff2 new file mode 100644 index 0000000..c092053 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-SemiBold.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-SemiBoldItalic.woff b/frontend/src/assets/fonts/Inter-SemiBoldItalic.woff new file mode 100644 index 0000000..1bd17a1 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-SemiBoldItalic.woff differ diff --git a/frontend/src/assets/fonts/Inter-SemiBoldItalic.woff2 b/frontend/src/assets/fonts/Inter-SemiBoldItalic.woff2 new file mode 100644 index 0000000..3c67c54 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-SemiBoldItalic.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-Thin-BETA.woff b/frontend/src/assets/fonts/Inter-Thin-BETA.woff new file mode 100644 index 0000000..42be3bc Binary files /dev/null and b/frontend/src/assets/fonts/Inter-Thin-BETA.woff differ diff --git a/frontend/src/assets/fonts/Inter-Thin-BETA.woff2 b/frontend/src/assets/fonts/Inter-Thin-BETA.woff2 new file mode 100644 index 0000000..3b35a12 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-Thin-BETA.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-ThinItalic-BETA.woff b/frontend/src/assets/fonts/Inter-ThinItalic-BETA.woff new file mode 100644 index 0000000..9addfb6 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-ThinItalic-BETA.woff differ diff --git a/frontend/src/assets/fonts/Inter-ThinItalic-BETA.woff2 b/frontend/src/assets/fonts/Inter-ThinItalic-BETA.woff2 new file mode 100644 index 0000000..c8ecf3a Binary files /dev/null and b/frontend/src/assets/fonts/Inter-ThinItalic-BETA.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-italic.var.woff2 b/frontend/src/assets/fonts/Inter-italic.var.woff2 new file mode 100644 index 0000000..2300488 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-italic.var.woff2 differ diff --git a/frontend/src/assets/fonts/Inter-upright.var.woff2 b/frontend/src/assets/fonts/Inter-upright.var.woff2 new file mode 100644 index 0000000..37d9900 Binary files /dev/null and b/frontend/src/assets/fonts/Inter-upright.var.woff2 differ diff --git a/frontend/src/assets/fonts/Inter.var.woff2 b/frontend/src/assets/fonts/Inter.var.woff2 new file mode 100644 index 0000000..1c91452 Binary files /dev/null and b/frontend/src/assets/fonts/Inter.var.woff2 differ diff --git a/frontend/src/components/AuthenticatedImage.vue b/frontend/src/components/AuthenticatedImage.vue index 60e4c38..5f3f0f3 100644 --- a/frontend/src/components/AuthenticatedImage.vue +++ b/frontend/src/components/AuthenticatedImage.vue @@ -1,8 +1,15 @@ + + \ No newline at end of file diff --git a/frontend/src/components/inputs/BadgeSelectField.vue b/frontend/src/components/BadgeSelectField.vue similarity index 97% rename from frontend/src/components/inputs/BadgeSelectField.vue rename to frontend/src/components/BadgeSelectField.vue index d8c8e2e..2ad407b 100644 --- a/frontend/src/components/inputs/BadgeSelectField.vue +++ b/frontend/src/components/BadgeSelectField.vue @@ -20,6 +20,7 @@ + @@ -133,6 +134,7 @@ export default { return this.value; }, set(value) { + console.log("set", value); } } }, diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue index 8ec828c..885e108 100644 --- a/frontend/src/components/BaseLayout.vue +++ b/frontend/src/components/BaseLayout.vue @@ -58,6 +58,7 @@ export default { }, }, } + - - \ No newline at end of file diff --git a/frontend/src/components/inputs/CameraFileSource.vue b/frontend/src/components/CameraFileSource.vue similarity index 100% rename from frontend/src/components/inputs/CameraFileSource.vue rename to frontend/src/components/CameraFileSource.vue diff --git a/frontend/src/components/CombinedFileField.vue b/frontend/src/components/CombinedFileField.vue index d8d1aec..c5430b0 100644 --- a/frontend/src/components/CombinedFileField.vue +++ b/frontend/src/components/CombinedFileField.vue @@ -3,10 +3,10 @@

Create New

Update

    -
  • +
  • {{ file.name }}
  • -
  • +
  • {{ file.name }}
@@ -100,26 +100,23 @@ \ No newline at end of file diff --git a/frontend/src/components/Notifications.vue b/frontend/src/components/Notifications.vue index 4495f46..9ed61b3 100644 --- a/frontend/src/components/Notifications.vue +++ b/frontend/src/components/Notifications.vue @@ -4,7 +4,7 @@
{{ - top_notifications.length + top_notifications.length }}
@@ -96,13 +96,4 @@ export default { \ No newline at end of file diff --git a/frontend/src/components/PropertyField.vue b/frontend/src/components/PropertyField.vue index e89234c..7681d16 100644 --- a/frontend/src/components/PropertyField.vue +++ b/frontend/src/components/PropertyField.vue @@ -19,9 +19,7 @@ import * as BIcons from "bootstrap-icons-vue"; import {mapActions, mapState} from "vuex"; import PropertyBadge from "@/components/PropertyBadge.vue"; -//import BadgeSelectField from "@/../extras/components/inputs/BadgeSelectField.vue"; -import BadgeSelectField from "@/components/inputs/BadgeSelectField.vue"; - +import BadgeSelectField from "@/components/BadgeSelectField.vue"; export default { name: "PropertyField", diff --git a/frontend/src/components/SearchBox.vue b/frontend/src/components/SearchBox.vue index 2287ee7..ad7a323 100644 --- a/frontend/src/components/SearchBox.vue +++ b/frontend/src/components/SearchBox.vue @@ -1,8 +1,7 @@