-
-
Registration
-
+{% extends 'base/layout.html' %}
+{% block title %}Registration{% endblock %}
+{% block content %}
+
+
+
+
+ {% if h_captcha_key %}
+
+ {% endif %}
+
+ {% if LEGAL_ENABLED %}
+
+ {% endif %}
+
+{% endblock %}
+{% block extra_head %}
+{% if h_captcha_key %}
+
+{% endif %}
+{% endblock %}
diff --git a/rootfs/api/templates/user/registration_disable.html b/rootfs/api/templates/user/registration_disable.html
index 8dfc922..4f4bbaf 100644
--- a/rootfs/api/templates/user/registration_disable.html
+++ b/rootfs/api/templates/user/registration_disable.html
@@ -1,5 +1,5 @@
{% extends 'user/message.html' %}
-{% block title %}Registration fail{% endblock %}
+{% block heading %}Registration fail{% endblock %}
{% block message %}
The registration function of drycc passport is not enabled. Please contact the administrator to sign up.
{% endblock %}
\ No newline at end of file
diff --git a/rootfs/api/templates/user/registration_done.html b/rootfs/api/templates/user/registration_done.html
index e6467c5..29497d0 100644
--- a/rootfs/api/templates/user/registration_done.html
+++ b/rootfs/api/templates/user/registration_done.html
@@ -1,5 +1,5 @@
{% extends 'user/message.html' %}
-{% block title %}Registration successful{% endblock %}
+{% block heading %}Registration successful{% endblock %}
{% block message %}
We have sent an email to your mailbox, please log in the email to activate your account.
{% endblock %}
\ No newline at end of file
diff --git a/rootfs/api/tests/__init__.py b/rootfs/api/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/rootfs/api/tests/test_create_oauth2_application.py b/rootfs/api/tests/test_create_oauth2_application.py
new file mode 100644
index 0000000..cead11f
--- /dev/null
+++ b/rootfs/api/tests/test_create_oauth2_application.py
@@ -0,0 +1,139 @@
+import json
+import os
+import tempfile
+from io import StringIO
+from unittest import mock
+
+from django.contrib.auth import get_user_model
+from django.contrib.auth.hashers import check_password
+from django.core.management import call_command
+from django.test import TestCase
+from oauth2_provider.models import get_application_model
+
+from api.management.commands import create_oauth2_application as cmd_module
+
+User = get_user_model()
+Application = get_application_model()
+
+
+class CreateOauth2ApplicationTestCase(TestCase):
+ def setUp(self):
+ self.admin = User.objects.create_superuser(
+ username='create-oauth2-admin',
+ email='create-oauth2-admin@example.com',
+ password='password123',
+ )
+ self.tmpdir = tempfile.mkdtemp()
+ self.secrets_dir = tempfile.mkdtemp()
+ self._secrets_patch = mock.patch.object(
+ cmd_module, 'secrets_path', self.secrets_dir,
+ )
+ self._secrets_patch.start()
+ self.addCleanup(self._secrets_patch.stop)
+
+ def _write_init_file(self, items):
+ path = os.path.join(self.tmpdir, 'init-applications.json')
+ with open(path, 'w') as f:
+ json.dump(items, f)
+ return path
+
+ def _write_secret_file(self, name, suffix, value):
+ path = os.path.join(
+ self.secrets_dir, 'drycc-passport-%s-%s' % (name, suffix))
+ with open(path, 'w') as f:
+ f.write(value)
+ return path
+
+ def _run(self, items):
+ path = self._write_init_file(items)
+ out = StringIO()
+ call_command('create_oauth2_application', '--path', path, stdout=out)
+ return out.getvalue()
+
+ def _m2m_app(self, **overrides):
+ item = {
+ 'name': 'builder',
+ 'key': '',
+ 'secret': '',
+ 'prefix': '',
+ 'grant_type': 'client-credentials',
+ 'client_type': 'confidential',
+ 'allowed_scopes': 'controller:hook',
+ 'redirect_uri': '',
+ }
+ item.update(overrides)
+ return item
+
+ def test_m2m_reads_credentials_from_mounted_secret_files(self):
+ self._write_secret_file('builder', 'key', 'mounted-key-value')
+ self._write_secret_file('builder', 'secret', 'mounted-secret-value\n')
+
+ self._run([self._m2m_app()])
+
+ app = Application.objects.get(name='builder')
+ self.assertEqual(app.client_id, 'mounted-key-value')
+ self.assertTrue(check_password('mounted-secret-value', app.client_secret))
+
+ def test_subdomain_app_reads_credentials_from_mounted_secret_files(self):
+ self._write_secret_file('grafana', 'key', 'gf-key')
+ self._write_secret_file('grafana', 'secret', 'gf-secret')
+
+ self._run([{
+ 'name': 'grafana',
+ 'key': '',
+ 'secret': '',
+ 'prefix': 'drycc-grafana',
+ 'grant_type': 'internal',
+ 'client_type': 'confidential',
+ 'allowed_scopes': 'openid profile email',
+ 'redirect_uri': '/oauth2/callback',
+ }])
+
+ app = Application.objects.get(name='grafana')
+ self.assertEqual(app.client_id, 'gf-key')
+ self.assertTrue(check_password('gf-secret', app.client_secret))
+
+ def test_falls_back_to_random_when_no_mount_and_no_init_value(self):
+ self._run([self._m2m_app()])
+
+ app = Application.objects.get(name='builder')
+ self.assertEqual(len(app.client_id), 40)
+ self.assertNotEqual(app.client_id, '')
+ self.assertNotEqual(app.client_secret, '')
+
+ def test_init_config_value_takes_precedence_over_mount(self):
+ self._write_secret_file('builder', 'key', 'mount-key')
+ self._write_secret_file('builder', 'secret', 'mount-secret')
+
+ self._run([self._m2m_app(key='cfg-key', secret='cfg-secret')])
+
+ app = Application.objects.get(name='builder')
+ self.assertEqual(app.client_id, 'cfg-key')
+ self.assertTrue(check_password('cfg-secret', app.client_secret))
+
+ def test_idempotent_when_mount_unchanged(self):
+ self._write_secret_file('builder', 'key', 'stable-key')
+ self._write_secret_file('builder', 'secret', 'stable-secret')
+
+ self._run([self._m2m_app()])
+ first = Application.objects.get(name='builder')
+ first_id = first.client_id
+ first_hashed_secret = first.client_secret
+
+ self._run([self._m2m_app()])
+ second = Application.objects.get(name='builder')
+
+ self.assertEqual(second.client_id, first_id)
+ self.assertEqual(second.client_secret, first_hashed_secret)
+ self.assertTrue(check_password('stable-secret', second.client_secret))
+ self.assertEqual(Application.objects.filter(name='builder').count(), 1)
+
+ def test_logs_credential_source(self):
+ self._write_secret_file('builder', 'key', 'k')
+ self._write_secret_file('builder', 'secret', 's')
+
+ out = self._run([self._m2m_app()])
+
+ self.assertIn('[builder/key] credential source: mounted-file', out)
+ self.assertIn('[builder/secret] credential source: mounted-file', out)
+ self.assertIn('Drycc builder app created', out)
diff --git a/rootfs/api/tests/test_pipelines.py b/rootfs/api/tests/test_pipelines.py
new file mode 100644
index 0000000..78b6a62
--- /dev/null
+++ b/rootfs/api/tests/test_pipelines.py
@@ -0,0 +1,117 @@
+from unittest.mock import Mock, patch
+
+from django.contrib.auth import get_user_model
+from django.test import RequestFactory, TestCase
+from social_django.models import UserSocialAuth
+
+from api.apps_extra.social_core.pipelines import (
+ handle_authenticated_binding, require_username_password
+)
+
+User = get_user_model()
+
+
+class PipelineTestCase(TestCase):
+ def setUp(self):
+ self.factory = RequestFactory()
+ self.user = User.objects.create_user(
+ username='pipe-user',
+ email='pipe@example.com',
+ password='password123',
+ )
+
+ def _make_backend(self, request, name='feishu'):
+ strategy = Mock()
+ strategy.request = request
+ strategy.session_set = Mock()
+ backend = Mock()
+ backend.name = name
+ backend.strategy = strategy
+ return backend
+
+ @patch(
+ 'api.apps_extra.social_core.pipelines.get_oauth_callback',
+ return_value='/oauth/callback?status=linked'
+ )
+ def test_handle_authenticated_binding_creates_identity(self, _mock_callback):
+ request = self.factory.get('/oauth/callback')
+ request.user = self.user
+ backend = self._make_backend(request)
+
+ response = handle_authenticated_binding(
+ backend, 'uid-1', {}, {'email': 'pipe@example.com'}
+ )
+
+ self.assertEqual(response.status_code, 302)
+ self.assertTrue(UserSocialAuth.objects.filter(
+ user=self.user, provider='feishu', uid='uid-1'
+ ).exists())
+
+ @patch(
+ 'api.apps_extra.social_core.pipelines.get_oauth_callback',
+ return_value='/oauth/callback?status=conflict'
+ )
+ def test_handle_authenticated_binding_conflict(self, _mock_callback):
+ other = User.objects.create_user(
+ username='other', email='other@example.com', password='password123'
+ )
+ UserSocialAuth.objects.create(
+ user=other, provider='feishu', uid='uid-1', extra_data={}
+ )
+ request = self.factory.get('/oauth/callback')
+ request.user = self.user
+ backend = self._make_backend(request)
+
+ response = handle_authenticated_binding(
+ backend, 'uid-1', {}, {'email': 'pipe@example.com'}
+ )
+
+ self.assertEqual(response.status_code, 302)
+
+ @patch(
+ 'api.apps_extra.social_core.pipelines.get_oauth_callback',
+ return_value='/oauth/callback?status=already_linked'
+ )
+ def test_handle_authenticated_binding_already_linked(self, _mock_callback):
+ UserSocialAuth.objects.create(
+ user=self.user, provider='feishu', uid='uid-1', extra_data={}
+ )
+ request = self.factory.get('/oauth/callback')
+ request.user = self.user
+ backend = self._make_backend(request)
+
+ response = handle_authenticated_binding(
+ backend, 'uid-1', {}, {'email': 'pipe@example.com'}
+ )
+
+ self.assertEqual(response.status_code, 302)
+
+ @patch(
+ 'api.apps_extra.social_core.pipelines.get_oauth_callback',
+ return_value='/oauth/callback?status=pending'
+ )
+ def test_require_username_password_sets_pending_session(self, _mock_callback):
+ request = self.factory.get('/oauth/callback')
+ request.user = Mock(is_authenticated=False)
+ backend = self._make_backend(request)
+
+ response = require_username_password(
+ backend, 'uid-1', {'email': 'pipe@example.com'},
+ {'email': 'pipe@example.com'}
+ )
+
+ self.assertEqual(response.status_code, 302)
+ backend.strategy.session_set.assert_called_once()
+
+ @patch(
+ 'api.apps_extra.social_core.pipelines.get_oauth_callback',
+ return_value='/oauth/callback?status=missing_email'
+ )
+ def test_require_username_password_requires_email(self, _mock_callback):
+ request = self.factory.get('/oauth/callback')
+ request.user = Mock(is_authenticated=False)
+ backend = self._make_backend(request)
+
+ response = require_username_password(backend, 'uid-1', {}, {})
+
+ self.assertEqual(response.status_code, 302)
diff --git a/rootfs/api/tests/test_serializers.py b/rootfs/api/tests/test_serializers.py
new file mode 100644
index 0000000..a25ddf8
--- /dev/null
+++ b/rootfs/api/tests/test_serializers.py
@@ -0,0 +1,50 @@
+from django.contrib.auth import get_user_model
+from django.test import TestCase
+
+from api.models import Message, MessagePreference
+from api.serializers import ListSerializer, MessagePreferenceSerializer, MessageSerializer
+
+User = get_user_model()
+
+
+class SerializerTestCase(TestCase):
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='serializer-user',
+ email='serializer@example.com',
+ password='password123',
+ )
+ self.message = Message.objects.create(
+ user=self.user,
+ category='system',
+ title='Serializer message',
+ content='Serializer content',
+ full_content='Serializer full content',
+ severity='info',
+ )
+ self.preference = MessagePreference.objects.create(
+ user=self.user,
+ email_alerts=True,
+ push_alerts=False,
+ webhook_url='',
+ notify_security=True,
+ notify_system=True,
+ notify_product=True,
+ notify_alert=True,
+ notify_service=True,
+ )
+
+ def test_list_serializer_validate_section(self):
+ result = ListSerializer.validate_section('1,2')
+ self.assertEqual(len(result), 2)
+
+ def test_message_serializer_contains_expected_fields(self):
+ data = MessageSerializer(self.message).data
+ self.assertEqual(data['category'], 'system')
+ self.assertEqual(data['title'], 'Serializer message')
+ self.assertIn('date', data)
+
+ def test_message_preference_serializer_contains_singular_alert_field(self):
+ data = MessagePreferenceSerializer(self.preference).data
+ self.assertIn('notify_alert', data)
+ self.assertTrue(data['notify_alert'])
diff --git a/rootfs/api/tests/test_tasks.py b/rootfs/api/tests/test_tasks.py
new file mode 100644
index 0000000..87fffb6
--- /dev/null
+++ b/rootfs/api/tests/test_tasks.py
@@ -0,0 +1,118 @@
+from unittest.mock import patch
+
+from django.contrib.auth import get_user_model
+from django.test import TestCase, override_settings
+
+from api.models import Message, MessagePreference
+from api.tasks import send_notification
+
+User = get_user_model()
+
+
+@override_settings(EMAIL_HOST='smtp.example.com')
+class NotificationTaskTestCase(TestCase):
+ def setUp(self):
+ delay_patcher = patch('api.tasks.send_notification.delay')
+ self.addCleanup(delay_patcher.stop)
+ self.mock_delay = delay_patcher.start()
+
+ self.user = User.objects.create_user(
+ username='task-user',
+ email='task@example.com',
+ password='password123',
+ )
+ self.preference = MessagePreference.objects.create(
+ user=self.user,
+ email_alerts=True,
+ push_alerts=True,
+ webhook_url='https://example.com/webhook',
+ notify_security=True,
+ notify_system=True,
+ notify_product=True,
+ notify_alert=True,
+ notify_service=True,
+ )
+ self.message = Message.objects.create(
+ user=self.user,
+ category='security',
+ title='Security event',
+ content='A security event occurred',
+ full_content='A security event occurred in detail',
+ severity='warning',
+ )
+
+ @patch('api.tasks.send_email_notification')
+ @patch('api.tasks.send_webhook_notification')
+ def test_send_notification_uses_enabled_channels(self, mock_webhook, mock_email):
+ send_notification.run(self.message)
+
+ mock_email.assert_called_once()
+ mock_webhook.assert_called_once_with(
+ mock_email.call_args[0][0], 'https://example.com/webhook'
+ )
+
+ @patch('api.tasks.send_email_notification')
+ @patch('api.tasks.send_webhook_notification')
+ def test_send_notification_respects_category_preference(self, mock_webhook, mock_email):
+ self.preference.notify_security = False
+ self.preference.save(update_fields=['notify_security'])
+
+ send_notification.run(self.message)
+
+ mock_email.assert_not_called()
+ mock_webhook.assert_not_called()
+
+ @patch('api.tasks.send_email_notification')
+ @patch('api.tasks.send_webhook_notification')
+ def test_send_notification_uses_default_true_for_unknown_category(
+ self, mock_webhook, mock_email
+ ):
+ self.message.category = 'unknown'
+ self.message.save(update_fields=['category'])
+
+ send_notification.run(self.message)
+
+ mock_email.assert_called_once()
+ mock_webhook.assert_called_once_with(
+ mock_email.call_args[0][0], 'https://example.com/webhook'
+ )
+
+ @patch('api.tasks.send_email_notification')
+ @patch('api.tasks.send_webhook_notification')
+ @patch('api.tasks.logger')
+ def test_send_notification_without_preference_sends_email_only(
+ self, mock_logger, mock_webhook, mock_email
+ ):
+ self.preference.delete()
+ # Refresh to clear cached related object
+ self.message.refresh_from_db()
+ self.message.user = User.objects.get(pk=self.user.pk)
+
+ send_notification.run(self.message)
+
+ mock_email.assert_called_once()
+ mock_webhook.assert_not_called()
+ mock_logger.info.assert_called_once_with(
+ 'Send email notification for message %s without user preference',
+ self.message.id,
+ )
+
+ @patch('api.tasks.send_webhook_notification', side_effect=RuntimeError('boom'))
+ def test_send_notification_propagates_dispatch_error(self, _mock_webhook):
+ with self.assertRaises(RuntimeError):
+ send_notification.run(self.message)
+
+ @patch('api.tasks.send_email_notification')
+ @patch('api.tasks.send_webhook_notification')
+ def test_send_notification_supports_alert_category_with_singular_field(
+ self, mock_webhook, mock_email
+ ):
+ self.message.category = 'alert'
+ self.message.save(update_fields=['category'])
+ self.preference.notify_alert = False
+ self.preference.save(update_fields=['notify_alert'])
+
+ send_notification.run(self.message)
+
+ mock_email.assert_not_called()
+ mock_webhook.assert_not_called()
diff --git a/rootfs/api/tests/test_utils.py b/rootfs/api/tests/test_utils.py
new file mode 100644
index 0000000..fd9201e
--- /dev/null
+++ b/rootfs/api/tests/test_utils.py
@@ -0,0 +1,105 @@
+from unittest.mock import Mock, patch
+
+from django.contrib.auth import get_user_model
+from django.test import RequestFactory, TestCase, override_settings
+from social_django.models import UserSocialAuth
+from rest_framework.exceptions import ValidationError
+
+from api.models import Message
+from api.utils import (
+ get_local_host,
+ get_oauth_callback,
+ get_user_socials,
+ send_activation_email,
+ send_email_notification,
+ send_webhook_notification,
+ timestamp2datetime,
+ validate_reserved_names,
+)
+
+User = get_user_model()
+
+
+class UtilsTestCase(TestCase):
+ def setUp(self):
+ self.factory = RequestFactory()
+ self.user = User.objects.create_user(
+ username='util-user',
+ email='util@example.com',
+ password='password123',
+ )
+ self.message = Message.objects.create(
+ user=self.user,
+ category='security',
+ title='Utility message',
+ content='Utility content',
+ full_content='Utility content full',
+ severity='warning',
+ )
+
+ def test_timestamp2datetime(self):
+ dt = timestamp2datetime(0)
+ self.assertEqual(dt.year, 1970)
+
+ def test_get_local_host(self):
+ request = self.factory.get('/user/info', secure=True, HTTP_HOST='example.com')
+ self.assertEqual(get_local_host(request), 'https://example.com')
+
+ @override_settings(SOCIAL_AUTH_LOGIN_CALLBACK_URL='/oauth/callback')
+ def test_get_oauth_callback(self):
+ request = self.factory.get('/user/login', secure=True, HTTP_HOST='example.com')
+ callback = get_oauth_callback(request, 'linked', 'feishu')
+ self.assertEqual(
+ callback,
+ 'https://example.com/oauth/callback?status=linked&provider=feishu'
+ )
+
+ @override_settings(RESERVED_NAME_PATTERNS=[r'^admin$', r'^root$'])
+ def test_validate_reserved_names_rejects_reserved(self):
+ with self.assertRaises(ValidationError):
+ validate_reserved_names('admin')
+
+ @override_settings(RESERVED_NAME_PATTERNS=[r'^admin$', r'^root$'])
+ def test_validate_reserved_names_accepts_normal_name(self):
+ validate_reserved_names('normal-user')
+
+ @override_settings(EMAIL_HOST='smtp.example.com')
+ @patch('api.utils.render_to_string', return_value='rendered body')
+ def test_send_activation_email(self, mock_render):
+ request = self.factory.get('/user/registration', secure=True, HTTP_HOST='example.com')
+ with patch.object(self.user, 'email_user') as mock_email_user:
+ send_activation_email(request, self.user)
+
+ mock_render.assert_called_once()
+ mock_email_user.assert_called_once()
+
+ @override_settings(EMAIL_HOST='smtp.example.com')
+ @patch('api.utils.render_to_string', return_value='
Hello')
+ def test_send_email_notification(self, mock_render):
+ with patch.object(self.user, 'email_user') as mock_email_user:
+ send_email_notification(self.message)
+
+ mock_render.assert_called_once()
+ mock_email_user.assert_called_once()
+
+ @patch('api.utils.requests.post')
+ def test_send_webhook_notification(self, mock_post):
+ mock_post.return_value.raise_for_status = Mock()
+
+ send_webhook_notification(self.message, 'https://example.com/webhook')
+
+ mock_post.assert_called_once()
+
+ @patch('api.apps_extra.social_core.backends.__all__', new=[])
+ def test_get_user_socials_without_backend_metadata(self):
+ UserSocialAuth.objects.create(
+ user=self.user,
+ provider='feishu',
+ uid='uid-1',
+ extra_data={'email': 'social@example.com'},
+ )
+
+ results = get_user_socials(self.user)
+
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0]['email'], 'social@example.com')
diff --git a/rootfs/api/tests/test_views.py b/rootfs/api/tests/test_views.py
new file mode 100644
index 0000000..52017a4
--- /dev/null
+++ b/rootfs/api/tests/test_views.py
@@ -0,0 +1,867 @@
+"""
+Test cases for API views
+"""
+from unittest.mock import patch
+
+from django.test import TestCase, RequestFactory
+from django.contrib.auth import get_user_model
+from django.urls import reverse
+from rest_framework.test import APITestCase
+from rest_framework import status
+from oauth2_provider.models import AccessToken
+from social_django.models import UserSocialAuth
+from django.core.cache import cache
+
+from api.views import (
+ ReadinessCheckView, LivenessCheckView, ActivateAccount
+)
+from api.forms import RegistrationForm
+from api.models import Message, MessagePreference
+
+User = get_user_model()
+
+
+class TestReadinessCheckView(TestCase):
+ """Test readiness check view"""
+
+ def test_readiness_check_get(self):
+ request = RequestFactory().get('/readiness/')
+ response = ReadinessCheckView.as_view()(request)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.content.decode(), 'OK')
+
+ def test_readiness_check_head(self):
+ request = RequestFactory().head('/readiness/')
+ response = ReadinessCheckView.as_view()(request)
+ self.assertEqual(response.status_code, 200)
+
+
+class TestLivenessCheckView(TestCase):
+ """Test liveness check view"""
+
+ def test_liveness_check_get(self):
+ request = RequestFactory().get('/liveness/')
+ response = LivenessCheckView.as_view()(request)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.content.decode(), 'OK')
+
+ def test_liveness_check_head(self):
+ request = RequestFactory().head('/liveness/')
+ response = LivenessCheckView.as_view()(request)
+ self.assertEqual(response.status_code, 200)
+
+
+class TestRegistrationView(TestCase):
+ """Test registration view"""
+
+ def setUp(self):
+ self.factory = RequestFactory()
+
+ def test_registration_form_valid(self):
+ form_data = {
+ 'username': 'testuser',
+ 'email': 'test@example.com',
+ 'password1': 'testpassword123',
+ 'password2': 'testpassword123'
+ }
+ form = RegistrationForm(data=form_data)
+ self.assertTrue(form.is_valid())
+
+ @patch('api.views.web.settings.REGISTRATION_ENABLED', False)
+ def test_registration_get_disabled(self):
+ response = self.client.get(reverse('registration'))
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(
+ response,
+ 'The registration function of drycc passport is not enabled.',
+ status_code=200,
+ )
+
+ @patch('api.views.web.settings.REGISTRATION_ENABLED', True)
+ @patch('api.views.web.settings.EMAIL_HOST', '')
+ def test_registration_post_creates_active_user_when_email_disabled(self):
+ response = self.client.post(reverse('registration'), {
+ 'username': 'newuser',
+ 'email': 'new@example.com',
+ 'password1': 'testpassword123',
+ 'password2': 'testpassword123',
+ 'h-captcha-response': 'PASSED',
+ })
+
+ self.assertEqual(response.status_code, 302)
+ user = User.objects.get(username='newuser')
+ self.assertTrue(user.is_active)
+
+ @patch('api.views.web.settings.REGISTRATION_ENABLED', True)
+ @patch('api.views.web.settings.EMAIL_HOST', 'smtp.example.com')
+ @patch('api.views.web.send_activation_email')
+ def test_registration_post_sends_activation_email_when_email_enabled(
+ self, mock_send_activation_email
+ ):
+ response = self.client.post(reverse('registration'), {
+ 'username': 'inactiveuser',
+ 'email': 'inactive@example.com',
+ 'password1': 'testpassword123',
+ 'password2': 'testpassword123',
+ 'h-captcha-response': 'PASSED',
+ })
+
+ self.assertEqual(response.status_code, 302)
+ user = User.objects.get(username='inactiveuser')
+ self.assertFalse(user.is_active)
+ mock_send_activation_email.assert_called_once()
+
+
+class TestActivateAccount(TestCase):
+ """Test account activation view"""
+
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='testuser',
+ email='test@example.com',
+ password='testpass123',
+ is_active=False
+ )
+ self.factory = RequestFactory()
+
+ def test_activate_account_invalid_token(self):
+ from django.utils.http import urlsafe_base64_encode
+ from django.utils.encoding import force_bytes
+ from django.contrib.messages.storage.fallback import FallbackStorage
+
+ uid = urlsafe_base64_encode(force_bytes(self.user.pk))
+ request = self.factory.get(f'/activate/{uid}/invalid-token/')
+ setattr(request, 'session', 'session')
+ messages = FallbackStorage(request)
+ setattr(request, '_messages', messages)
+ response = ActivateAccount.as_view()(request, uidb64=uid, token='invalid-token')
+
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, '/user/activate/fail/')
+
+ def test_activate_account_valid_token(self):
+ from django.utils.http import urlsafe_base64_encode
+ from django.utils.encoding import force_bytes
+ from django.contrib.messages.storage.fallback import FallbackStorage
+ from api.utils import token_generator
+
+ uid = urlsafe_base64_encode(force_bytes(self.user.pk))
+ token = token_generator.make_token(self.user)
+ request = self.factory.get(f'/activate/{uid}/{token}/')
+ request.session = self.client.session
+ messages = FallbackStorage(request)
+ setattr(request, '_messages', messages)
+
+ response = ActivateAccount.as_view()(request, uidb64=uid, token=token)
+
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, '/user/activate/done/')
+ self.user.refresh_from_db()
+ self.assertTrue(self.user.is_active)
+
+
+class TestUserDetailView(APITestCase):
+ """Test user detail viewset"""
+
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='testuser',
+ email='test@example.com',
+ password='testpass123'
+ )
+ self.client.force_authenticate(user=self.user)
+
+ def test_get_user_detail(self):
+ url = '/user/info'
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['username'], 'testuser')
+
+ @patch('api.views.api.settings.EMAIL_HOST', 'smtp.example.com')
+ @patch('api.views.api.render_to_string', return_value='mail-body')
+ def test_update_user_detail_sends_confirmation_email_and_caches_data(self, _mock_render):
+ url = '/user/info'
+ response = self.client.put(url, {'first_name': 'Updated'})
+
+ self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+ self.user.refresh_from_db()
+ self.assertEqual(self.user.first_name, '')
+ self.assertEqual(cache.get(f'user:serializer:{self.user.pk}'), {'first_name': 'Updated'})
+
+ def test_update_user_detail_fails_validation(self):
+ url = '/user/info'
+ response = self.client.put(url, {'email': 'invalid-email'})
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertIn('email', response.data)
+
+
+class TestUserTokensView(APITestCase):
+ """Test user tokens viewset"""
+
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='testuser',
+ email='test@example.com',
+ password='testpass123'
+ )
+ self.client.force_authenticate(user=self.user)
+ self.token = AccessToken.objects.create(
+ user=self.user,
+ token='tokentest',
+ application=None,
+ expires='9999-01-01T00:00:00Z',
+ scope='read write'
+ )
+
+ def test_list_user_tokens(self):
+ url = reverse('user_tokens')
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_delete_user_token(self):
+ url = reverse('user_grants', args=[self.token.id])
+ response = self.client.delete(url)
+ self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+ self.assertFalse(AccessToken.objects.filter(id=self.token.id).exists())
+
+
+class TestUserAccountPasswordView(APITestCase):
+ """Test user account password view"""
+
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='testuser',
+ email='test@example.com',
+ password='oldpassword123'
+ )
+ self.client.force_authenticate(user=self.user)
+
+ def test_update_password_valid(self):
+ url = '/user/password'
+ data = {
+ 'password': 'oldpassword123',
+ 'new_password': 'newpassword123'
+ }
+ response = self.client.put(url, data)
+ self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+
+ self.user.refresh_from_db()
+ self.assertTrue(self.user.check_password('newpassword123'))
+
+
+class TestIdentityProviderView(APITestCase):
+
+ @patch('api.views.api.settings.SOCIAL_AUTH_FEISHU_KEY', 'key')
+ @patch('api.views.api.settings.SOCIAL_AUTH_FEISHU_SECRET', 'secret')
+ @patch('api.views.api.settings.SOCIAL_AUTH_GOOGLE_KEY', '')
+ @patch('api.views.api.settings.SOCIAL_AUTH_GOOGLE_SECRET', '')
+ def test_only_configured_identity_providers_are_returned(self):
+ response = self.client.get(reverse('user_identity_providers'))
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertGreaterEqual(response.data['count'], 1)
+ provider_names = [item['name'] for item in response.data['results']]
+ self.assertIn('feishu', provider_names)
+ self.assertNotIn('google', provider_names)
+
+
+class TestUserIdentityView(APITestCase):
+
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='identity-user',
+ email='identity@example.com',
+ password='testpass123'
+ )
+ self.other_user = User.objects.create_user(
+ username='other-user',
+ email='other@example.com',
+ password='testpass123'
+ )
+ self.identity = UserSocialAuth.objects.create(
+ user=self.user,
+ provider='feishu',
+ uid='uid-1',
+ extra_data={'email': 'identity@example.com'}
+ )
+ self.other_identity = UserSocialAuth.objects.create(
+ user=self.other_user,
+ provider='github',
+ uid='uid-2',
+ extra_data={'email': 'other@example.com'}
+ )
+ self.client.force_authenticate(user=self.user)
+
+ def test_list_user_identities(self):
+ response = self.client.get(reverse('user_identities-list'))
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['count'], 1)
+ self.assertEqual(response.data['results'][0]['provider'], 'feishu')
+
+ def test_delete_own_identity(self):
+ response = self.client.delete(reverse('user_identities-detail', args=[self.identity.id]))
+
+ self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+ self.assertFalse(UserSocialAuth.objects.filter(id=self.identity.id).exists())
+
+ def test_cannot_delete_other_user_identity(self):
+ response = self.client.delete(
+ reverse('user_identities-detail', args=[self.other_identity.id])
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+
+class TestOAuthPendingAndCreateView(APITestCase):
+
+ def setUp(self):
+ self.client_handler = self.client.handler
+
+ def test_oauth_pending_without_session_data(self):
+ response = self.client.get(reverse('user_oauth_pending'))
+
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ def test_oauth_pending_with_session_data(self):
+ session = self.client.session
+ session['oauth_pending'] = {
+ 'provider': 'feishu',
+ 'email': 'pending@example.com',
+ 'uid': 'pending-uid',
+ 'extra_data': {'email': 'pending@example.com'},
+ }
+ session.save()
+
+ response = self.client.get(reverse('user_oauth_pending'))
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['provider'], 'feishu')
+ self.assertEqual(response.data['email'], 'pending@example.com')
+
+ def test_oauth_create_user_success(self):
+ session = self.client.session
+ session['oauth_pending'] = {
+ 'provider': 'feishu',
+ 'email': 'new@example.com',
+ 'uid': 'pending-uid',
+ 'extra_data': {'email': 'new@example.com'},
+ }
+ session.save()
+
+ response = self.client.post(reverse('user_oauth_create'), {
+ 'username': 'oauth-user',
+ 'password': 'password123',
+ })
+
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+ self.assertTrue(
+ User.objects.filter(username='oauth-user', email='new@example.com').exists()
+ )
+ self.assertTrue(
+ UserSocialAuth.objects.filter(provider='feishu', uid='pending-uid').exists()
+ )
+
+ def test_oauth_create_user_requires_pending(self):
+ response = self.client.post(reverse('user_oauth_create'), {
+ 'username': 'oauth-user',
+ 'password': 'password123',
+ })
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_oauth_create_user_requires_username(self):
+ session = self.client.session
+ session['oauth_pending'] = {
+ 'provider': 'feishu',
+ 'email': 'new@example.com',
+ 'uid': 'pending-uid',
+ 'extra_data': {'email': 'new@example.com'},
+ }
+ session.save()
+
+ response = self.client.post(reverse('user_oauth_create'), {
+ 'password': 'password123',
+ })
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_oauth_create_user_rejects_existing_email(self):
+ User.objects.create_user(
+ username='exists', email='new@example.com', password='password123'
+ )
+ session = self.client.session
+ session['oauth_pending'] = {
+ 'provider': 'feishu',
+ 'email': 'new@example.com',
+ 'uid': 'pending-uid',
+ 'extra_data': {'email': 'new@example.com'},
+ }
+ session.save()
+
+ response = self.client.post(reverse('user_oauth_create'), {
+ 'username': 'oauth-user',
+ 'password': 'password123',
+ })
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ def test_oauth_create_user_rejects_existing_identity(self):
+ existing_user = User.objects.create_user(
+ username='exists', email='exists@example.com', password='password123'
+ )
+ UserSocialAuth.objects.create(
+ user=existing_user,
+ provider='feishu',
+ uid='pending-uid',
+ extra_data={'email': 'exists@example.com'}
+ )
+ session = self.client.session
+ session['oauth_pending'] = {
+ 'provider': 'feishu',
+ 'email': 'new@example.com',
+ 'uid': 'pending-uid',
+ 'extra_data': {'email': 'new@example.com'},
+ }
+ session.save()
+
+ response = self.client.post(reverse('user_oauth_create'), {
+ 'username': 'oauth-user',
+ 'password': 'password123',
+ })
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+
+class TestUserMessageViews(APITestCase):
+
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='message-user',
+ email='message@example.com',
+ password='testpass123'
+ )
+ self.other_user = User.objects.create_user(
+ username='other-message-user',
+ email='other-message@example.com',
+ password='testpass123'
+ )
+ self.client.force_authenticate(user=self.user)
+ self.message1 = Message.objects.create(
+ user=self.user,
+ category='system',
+ title='System message',
+ content='System content',
+ severity='info'
+ )
+ self.message2 = Message.objects.create(
+ user=self.user,
+ category='security',
+ title='Security message',
+ content='Security content',
+ severity='warning'
+ )
+ self.other_message = Message.objects.create(
+ user=self.other_user,
+ category='system',
+ title='Other message',
+ content='Other content',
+ severity='info'
+ )
+
+ def test_list_messages(self):
+ response = self.client.get(reverse('user_messages-list'))
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['count'], 2)
+ self.assertIn('results', response.data)
+
+ def test_filter_messages_by_category(self):
+ response = self.client.get(reverse('user_messages-list'), {'category': 'security'})
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['count'], 1)
+ self.assertEqual(response.data['results'][0]['category'], 'security')
+
+ def test_list_messages_supports_limit_offset_pagination(self):
+ response = self.client.get(reverse('user_messages-list'), {'limit': 1, 'offset': 0})
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data['results']), 1)
+ self.assertEqual(response.data['count'], 2)
+
+ def test_service_message_create(self):
+ from api.models import Application
+ from urllib.parse import urlencode
+
+ # Setup application
+ Application.objects.create(
+ name='test_app',
+ client_type='confidential',
+ client_id='my_test_client_id',
+ client_secret='my_test_client_secret',
+ authorization_grant_type='client-credentials',
+ user=self.user
+ )
+
+ # 1. Fetch access token via client-credentials flow
+ data = urlencode({
+ 'grant_type': 'client_credentials',
+ 'client_id': 'my_test_client_id',
+ 'client_secret': 'my_test_client_secret',
+ 'scope': 'passport:message'
+ })
+
+ token_response = self.client.post(
+ '/oauth/token/',
+ data=data,
+ content_type='application/x-www-form-urlencoded'
+ )
+
+ self.assertEqual(token_response.status_code, 200, token_response.json())
+ access_token = token_response.json()['access_token']
+
+ # 2. Use the access token to create a message
+ response = self.client.post('/messages/', {
+ 'username': self.user.username,
+ 'category': 'alert',
+ 'title': 'Created from API',
+ 'content': 'API created content',
+ 'full_content': 'API created full content',
+ 'severity': 'warning',
+ 'is_read': False,
+ 'action_link': '/messages',
+ 'action_text': 'Open',
+ }, HTTP_AUTHORIZATION=f'Bearer {access_token}')
+
+ self.assertEqual(response.status_code, 201)
+ self.assertTrue(Message.objects.filter(user=self.user, title='Created from API').exists())
+
+ def test_mark_all_messages_as_read(self):
+ response = self.client.put(reverse('user_messages-mark-all-read'))
+
+ self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+ self.assertFalse(Message.objects.filter(user=self.user, is_read=False).exists())
+
+ def test_get_message_detail_marks_message_as_read(self):
+ response = self.client.get(reverse('user_messages-detail', args=[self.message1.id]))
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.message1.refresh_from_db()
+ self.assertTrue(self.message1.is_read)
+
+ def test_cannot_get_other_users_message(self):
+ response = self.client.get(reverse('user_messages-detail', args=[self.other_message.id]))
+
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ def test_filter_messages_by_is_read_false(self):
+ # message1 defaults to is_read=False; mark message2 as read
+ self.message2.is_read = True
+ self.message2.save()
+ response = self.client.get(reverse('user_messages-list'), {'is_read': 'false'})
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['count'], 1)
+ self.assertEqual(response.data['results'][0]['id'], self.message1.id)
+ self.assertFalse(response.data['results'][0]['is_read'])
+
+ def test_filter_messages_by_is_read_true(self):
+ # mark message1 as read while message2 remains unread
+ self.message1.is_read = True
+ self.message1.save()
+ response = self.client.get(reverse('user_messages-list'), {'is_read': 'true'})
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['count'], 1)
+ self.assertEqual(response.data['results'][0]['id'], self.message1.id)
+ self.assertTrue(response.data['results'][0]['is_read'])
+
+ def test_filter_messages_by_category_and_is_read(self):
+ # message1: category=system is_read=False;message2: category=security is_read=True
+ self.message2.is_read = True
+ self.message2.save()
+ response = self.client.get(
+ reverse('user_messages-list'),
+ {'category': 'system', 'is_read': 'false'}
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['count'], 1)
+ self.assertEqual(response.data['results'][0]['category'], 'system')
+ self.assertFalse(response.data['results'][0]['is_read'])
+
+ def test_filter_messages_without_is_read_returns_all(self):
+ # returns all messages when is_read is not provided
+ self.message1.is_read = True
+ self.message1.save()
+ response = self.client.get(reverse('user_messages-list'))
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['count'], 2)
+
+ def test_search_by_title(self):
+ response = self.client.get(reverse('user_messages-list'), {'search': 'System'})
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['count'], 1)
+ self.assertEqual(response.data['results'][0]['id'], self.message1.id)
+
+ def test_search_by_content(self):
+ response = self.client.get(reverse('user_messages-list'), {'search': 'Security content'})
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['count'], 1)
+ self.assertEqual(response.data['results'][0]['id'], self.message2.id)
+
+ def test_search_case_insensitive(self):
+ response = self.client.get(reverse('user_messages-list'), {'search': 'SYSTEM MESSAGE'})
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['count'], 1)
+ self.assertEqual(response.data['results'][0]['id'], self.message1.id)
+
+ def test_search_no_match_returns_empty(self):
+ response = self.client.get(reverse('user_messages-list'), {'search': 'zzznomatch'})
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['count'], 0)
+
+ def test_search_with_category_applies_and(self):
+ # search matches title+content, but category filter should restrict results to system
+ response = self.client.get(
+ reverse('user_messages-list'),
+ {'search': 'content', 'category': 'system'}
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['count'], 1)
+ self.assertEqual(response.data['results'][0]['id'], self.message1.id)
+
+ def test_search_empty_string_returns_all(self):
+ response = self.client.get(reverse('user_messages-list'), {'search': ''})
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['count'], 2)
+
+
+class TestUserMessagePreferenceView(APITestCase):
+
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='preference-user',
+ email='preference@example.com',
+ password='testpass123'
+ )
+ self.client.force_authenticate(user=self.user)
+
+ def test_get_creates_preference_if_missing(self):
+ response = self.client.get(reverse('user_message_preferences'))
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertTrue(MessagePreference.objects.filter(user=self.user).exists())
+
+ def test_put_updates_preference(self):
+ response = self.client.put(reverse('user_message_preferences'), {
+ 'email_alerts': False,
+ 'notify_alert': False,
+ })
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ preference = MessagePreference.objects.get(user=self.user)
+ self.assertFalse(preference.email_alerts)
+ self.assertFalse(preference.notify_alert)
+
+
+class TestUserLoginIdentityLinking(TestCase):
+
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='login-user',
+ email='login@example.com',
+ password='password123'
+ )
+
+ def test_login_links_pending_identity(self):
+ session = self.client.session
+ session['oauth_pending'] = {
+ 'provider': 'feishu',
+ 'uid': 'uid-100',
+ 'extra_data': {'email': 'login@example.com'},
+ }
+ session.save()
+
+ response = self.client.post(reverse('user_login') + '?identity_linking=1', {
+ 'username': 'login-user',
+ 'password': 'password123',
+ })
+
+ self.assertEqual(response.status_code, 302)
+ self.assertIn('status=linked', response.url)
+ self.assertTrue(UserSocialAuth.objects.filter(
+ user=self.user, provider='feishu', uid='uid-100'
+ ).exists())
+
+ def test_login_identity_linking_conflict_redirects(self):
+ other_user = User.objects.create_user(
+ username='other-login-user',
+ email='other-login@example.com',
+ password='password123'
+ )
+ UserSocialAuth.objects.create(
+ user=other_user,
+ provider='feishu',
+ uid='uid-100',
+ extra_data={'email': 'other-login@example.com'}
+ )
+ session = self.client.session
+ session['oauth_pending'] = {
+ 'provider': 'feishu',
+ 'uid': 'uid-100',
+ 'extra_data': {'email': 'login@example.com'},
+ }
+ session.save()
+
+ response = self.client.post(reverse('user_login') + '?identity_linking=1', {
+ 'username': 'login-user',
+ 'password': 'password123',
+ })
+
+ self.assertEqual(response.status_code, 302)
+ self.assertIn('status=conflict', response.url)
+
+ @patch('api.views.web.settings.EMAIL_HOST', 'smtp.example.com')
+ @patch('api.views.web.settings.REGISTRATION_ENABLED', True)
+ def test_login_page_context_shows_identity_linking_error(self):
+ response = self.client.post(reverse('user_login') + '?identity_linking=1', {
+ 'username': 'login-user',
+ 'password': 'wrong-password',
+ })
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'Identity linking login failed. Please try again.')
+
+ @patch('api.views.web.settings.EMAIL_HOST', '')
+ @patch('api.views.web.settings.REGISTRATION_ENABLED', False)
+ def test_login_page_context_flags_follow_settings(self):
+ response = self.client.get(reverse('user_login') + '?identity_linking=1')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'Identity Linking')
+
+
+class TestOAuthCallbackTemplateView(TestCase):
+
+ def test_get_without_pending_renders_error_state(self):
+ response = self.client.get(reverse('oauth_callback') + '?status=pending')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'OAuth flow error. Please try again.')
+
+ @patch('api.views.api.OAuthCreateUserView._create_oauth_user')
+ def test_post_password_mismatch_renders_error(self, _mock_create):
+ session = self.client.session
+ session['oauth_pending'] = {
+ 'provider': 'feishu',
+ 'email': 'pending@example.com',
+ }
+ session.save()
+
+ response = self.client.post(reverse('oauth_callback'), {
+ 'username': 'oauth-user',
+ 'password': 'password123',
+ 'confirm_password': 'different123',
+ })
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'Password confirmation does not match')
+
+ @patch('api.views.api.OAuthCreateUserView._create_oauth_user')
+ def test_post_success_redirects_account_setting(self, mock_create):
+ session = self.client.session
+ session['oauth_pending'] = {
+ 'provider': 'feishu',
+ 'email': 'pending@example.com',
+ }
+ session.save()
+
+ response = self.client.post(reverse('oauth_callback'), {
+ 'username': 'oauth-user',
+ 'password': 'password123',
+ 'confirm_password': 'password123',
+ })
+
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.url, '/account-setting')
+ mock_create.assert_called_once()
+
+
+class TestUpdateAccountView(TestCase):
+
+ def setUp(self):
+ self.user = User.objects.create_user(
+ username='update-user',
+ email='update@example.com',
+ password='password123'
+ )
+
+ def test_update_account_applies_cached_serializer_data(self):
+ from django.utils.http import urlsafe_base64_encode
+ from django.utils.encoding import force_bytes
+ from api.utils import token_generator
+
+ cache.set(f'user:serializer:{self.user.pk}', {'first_name': 'Cached'}, 1800)
+ uid = urlsafe_base64_encode(force_bytes(self.user.pk))
+ token = token_generator.make_token(self.user)
+
+ response = self.client.get(reverse('user_update_account', args=[uid, token]))
+
+ self.assertEqual(response.status_code, 200)
+ self.user.refresh_from_db()
+ self.assertEqual(self.user.first_name, 'Cached')
+ self.assertIsNone(cache.get(f'user:serializer:{self.user.pk}'))
+
+ def test_update_account_invalid_token_renders_fail(self):
+ from django.utils.http import urlsafe_base64_encode
+ from django.utils.encoding import force_bytes
+
+ uid = urlsafe_base64_encode(force_bytes(self.user.pk))
+ response = self.client.get(reverse('user_update_account', args=[uid, 'invalid-token']))
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'update fail', status_code=200)
+
+ def test_update_account_without_cached_data_renders_fail(self):
+ from django.utils.http import urlsafe_base64_encode
+ from django.utils.encoding import force_bytes
+ from api.utils import token_generator
+
+ uid = urlsafe_base64_encode(force_bytes(self.user.pk))
+ token = token_generator.make_token(self.user)
+ response = self.client.get(reverse('user_update_account', args=[uid, token]))
+
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'update fail', status_code=200)
+
+
+class TestPasswordResetViews(TestCase):
+
+ def test_password_reset_form_page_loads(self):
+ response = self.client.get(reverse('user_password_reset'))
+ self.assertEqual(response.status_code, 200)
+
+ def test_password_reset_done_page_loads(self):
+ response = self.client.get(reverse('user_password_reset_done'))
+ self.assertEqual(response.status_code, 200)
+
+ def test_password_reset_complete_page_loads(self):
+ response = self.client.get(reverse('user_password_reset_complete'))
+ self.assertEqual(response.status_code, 200)
+
+ def test_password_reset_confirm_invalid_token_page_loads(self):
+ response = self.client.get(
+ reverse('user_password_reset_confirm', args=['invalid', 'invalid-token'])
+ )
+ self.assertEqual(response.status_code, 200)
diff --git a/rootfs/api/urls.py b/rootfs/api/urls.py
index c1c1aeb..5d34651 100644
--- a/rootfs/api/urls.py
+++ b/rootfs/api/urls.py
@@ -1,51 +1,75 @@
from django.urls import re_path, include
-from rest_framework.routers import DefaultRouter
+from rest_framework.routers import SimpleRouter
-from api import views
+from api.views import web, api
+from passport.views import AppSettingsViewSet
-router = DefaultRouter(trailing_slash=False)
+
+class OptionalSlashRouter(SimpleRouter):
+ """Router that accepts both trailing-slash and no-trailing-slash URLs."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.trailing_slash = '/?'
+
+
+router = OptionalSlashRouter()
+router.register(r'user/messages', api.UserMessageViewSet, basename='user_messages')
+router.register(r'messages', api.ServiceMessageViewSet, basename='service_messages')
+router.register(r'user/identities', api.UserIdentityViewSet, basename='user_identities')
urlpatterns = [
re_path(r'^', include(router.urls)),
- re_path(r'^info/?$',
- views.UserDetailView.as_view({'get': 'retrieve', 'put': 'update'})),
- re_path(r'update/(?P
.+)/(?P.+)/?$',
- views.UpdateAccount.as_view(), name='user_update_account'),
- re_path(r'^avatar/(?P[-_\w]+)/?$',
- views.UserAvatarViewSet.as_view({'get': 'avatar'})),
- re_path(r'registration/?$', views.RegistrationView.as_view(), name='registration'),
- re_path(r'activate/(?P.+)/(?P.+)/?$',
- views.ActivateAccount.as_view(), name='user_activate_account'),
- re_path(r'registration/done/?$', views.RegistrationDoneView.as_view(),
+ # Settings URL (moved from main urls.py)
+ re_path(r'^settings/?$', AppSettingsViewSet.as_view({'get': 'retrieve'})),
+ # User URLs (add user prefix)
+ re_path(r'^user/info/?$',
+ api.UserDetailView.as_view({'get': 'retrieve', 'put': 'update'})),
+ re_path(r'^user/update/(?P.+)/(?P.+)/?$',
+ web.UpdateAccount.as_view(), name='user_update_account'),
+ re_path(r'^user/registration/?$', web.RegistrationView.as_view(), name='registration'),
+ re_path(r'^user/activate/(?P.+)/(?P.+)/?$',
+ web.ActivateAccount.as_view(), name='user_activate_account'),
+ re_path(r'^user/registration/done/?$', web.RegistrationDoneView.as_view(),
name='user_registration_done'),
- re_path(r'activate/done/?$', views.ActivateAccountDoneView.as_view(),
+ re_path(r'^user/activate/done/?$', web.ActivateAccountDoneView.as_view(),
name='user_activate_account_done'),
- re_path(r'activate/fail/?$', views.ActivateAccountFailView.as_view(),
+ re_path(r'^user/activate/fail/?$', web.ActivateAccountFailView.as_view(),
name='user_activate_account_done'),
- re_path(r'password_reset/?$', views.UserPasswordResetView.as_view(),
+ re_path(r'^oauth/callback/?$', web.OAuthCallbackTemplateView.as_view(),
+ name='oauth_callback'),
+ re_path(r'^user/password_reset/?$', web.UserPasswordResetView.as_view(),
name='user_password_reset'),
- re_path(r'password_reset/done/?$',
- views.UserPasswordResetDoneView.as_view(),
+ re_path(r'^user/password_reset/done/?$',
+ web.UserPasswordResetDoneView.as_view(),
name='user_password_reset_done'),
- re_path(r'reset/(?P.+)/(?P.+)/?$',
- views.UserPasswordResetConfirmView.as_view(),
+ re_path(r'^user/reset/(?P.+)/(?P.+)/?$',
+ web.UserPasswordResetConfirmView.as_view(),
name='user_password_reset_confirm'),
- re_path(r'reset/done/?$',
- views.UserPasswordResetCompleteView.as_view(),
+ re_path(r'^user/reset/done/?$',
+ web.UserPasswordResetCompleteView.as_view(),
name='user_password_reset_complete'),
- re_path(r'login/?$', views.UserLoginView.as_view(), name='user_login'),
- re_path(r'login/done/?$', views.LoginDoneView.as_view(), name='login_done'),
- re_path(r'logout/?$', views.UserLogoutView.as_view(), name='user_logout'),
-
- re_path(r'tokens/?$',
- views.UserTokensTemplateView.as_view({'get': 'retrieve'}),
+ re_path(r'^user/login/?$', web.UserLoginView.as_view(), name='user_login'),
+ re_path(r'^user/login/done/?$', web.LoginDoneView.as_view(), name='login_done'),
+ re_path(r'^user/logout/?$', web.UserLogoutView.as_view(), name='user_logout'),
+ re_path(r'^user/tokens/?$',
+ api.UserTokensView.as_view({'get': 'list'}),
name='user_tokens'),
- re_path(r'tokens/(?P.+)/?$',
- views.UserTokenDeleteView.as_view({'delete': 'destroy'}),
+ re_path(r'^user/tokens/(?P.+)/?$',
+ api.UserTokensView.as_view({'delete': 'destroy'}),
name='user_grants'),
- re_path(r'email/?$', views.UserEmailView.as_view({'get': 'retrieve'})),
- re_path(r'password/?$',
- views.UserAccountPasswordView.as_view({'put': 'update'}),
+ re_path(r'^user/email/?$', api.UserEmailView.as_view({'get': 'retrieve'})),
+ re_path(r'^user/password/?$',
+ api.UserAccountPasswordView.as_view(),
name='user_account_update_password'),
-
+ re_path(r'^user/identity-providers/?$',
+ api.IdentityProviderView.as_view(), name='user_identity_providers'),
+ re_path(r'^user/oauth/pending/?$',
+ api.OAuthPendingView.as_view(), name='user_oauth_pending'),
+ re_path(r'^user/oauth/create/?$',
+ api.OAuthCreateUserView.as_view(), name='user_oauth_create'),
+ re_path(r'^user/message-preferences/?$',
+ api.UserMessagePreferenceViewSet.as_view(
+ {'get': 'retrieve', 'put': 'update', 'patch': 'partial_update'}),
+ name='user_message_preferences'),
]
diff --git a/rootfs/api/utils.py b/rootfs/api/utils.py
index b76040c..5eeefeb 100644
--- a/rootfs/api/utils.py
+++ b/rootfs/api/utils.py
@@ -1,15 +1,24 @@
"""
Helper functions used by the Drycc Passport server.
"""
+import re
import logging
import datetime
+import requests
+from django.conf import settings
+
+from urllib.parse import urlencode
+
+from django.utils.html import strip_tags
from django.utils.encoding import force_bytes
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.shortcuts import render
+from rest_framework.exceptions import ValidationError
+
logger = logging.getLogger(__name__)
@@ -47,7 +56,57 @@ def get_local_host(request):
return uri[0:uri.find(request.path)]
+def get_user_socials(user):
+ from api.apps_extra.social_core import backends
+ provider_map = {
+ backend_cls.name: backend_cls
+ for backend_cls in backends.__all__
+ }
+ results = []
+ for social in user.social_auth.all():
+ backend_cls = provider_map.get(social.provider)
+ email = None
+ if social.extra_data:
+ email = social.extra_data.get('email') or social.extra_data.get('user_email')
+ results.append({
+ 'id': social.id,
+ 'provider': social.provider,
+ 'display_name': social.provider.title(),
+ 'uid': social.uid,
+ 'icon': backend_cls.icon if backend_cls else '',
+ 'email': email or user.email,
+ })
+ return results
+
+
+def get_oauth_callback(request, status, provider=None):
+ query = {'status': status}
+ domain = get_local_host(request)
+ if provider:
+ query['provider'] = provider
+ return f"{domain}{settings.SOCIAL_AUTH_LOGIN_CALLBACK_URL}?{urlencode(query)}"
+
+
+def validate_reserved_names(value):
+ """A value cannot use some reserved names."""
+ for reserved_name_pattern in settings.RESERVED_NAME_PATTERNS:
+ if re.match(reserved_name_pattern, value):
+ raise ValidationError('{} is a reserved name.'.format(value))
+
+
def send_activation_email(request, user):
+ if not user.email:
+ logger.warning(
+ "Activation email skipped for user %s due to missing email address",
+ user.username
+ )
+ return
+ if not getattr(settings, 'EMAIL_HOST', ''):
+ logger.warning(
+ "Activation email skipped for user %s due to missing SMTP configuration",
+ user.username
+ )
+ return
domain = get_local_host(request)
mail_subject = 'Activate your account.'
message = render_to_string(
@@ -60,6 +119,72 @@ def send_activation_email(request, user):
user.email_user(mail_subject, message, fail_silently=True)
+def send_email_notification(message) -> None:
+ """Send an email notification for a message.
+
+ Silently skips when SMTP / DEFAULT_FROM_EMAIL is not configured, or
+ when the target user has no email address. This avoids triggering
+ endless Celery retries in environments without a configured mail
+ server (e.g. local development).
+ """
+ if message.user.email is None:
+ logger.info(
+ "Email notification skipped for message %s due to missing user email",
+ message.id
+ )
+ return
+ if not getattr(settings, 'EMAIL_HOST', ''):
+ logger.info(
+ "Email notification skipped for message %s due to missing SMTP configuration",
+ message.id
+ )
+ return
+ mail_subject = f"[{message.get_category_display()}] {message.title}"
+ html_message = render_to_string(
+ 'notifications/email_message.html',
+ {
+ 'message': message,
+ 'user': message.user,
+ 'domain': getattr(settings, 'DASHBOARD_URL', '/'),
+ }
+ )
+ plain_message = strip_tags(html_message)
+ message.user.email_user(
+ subject=mail_subject,
+ message=plain_message,
+ html_message=html_message,
+ fail_silently=False,
+ )
+ logger.info("Email notification sent to %s for message %s", message.user.email, message.id)
+
+
+def send_webhook_notification(message, webhook_url) -> None:
+ """Send a webhook notification for a message."""
+ if not webhook_url:
+ return
+ payload = {
+ 'id': str(message.id),
+ 'category': message.category,
+ 'title': message.title,
+ 'content': message.content,
+ 'severity': message.severity,
+ 'created_at': message.created_at.isoformat() if message.created_at else None,
+ 'user': {
+ 'id': message.user.id,
+ 'username': message.user.username,
+ 'email': message.user.email,
+ },
+ }
+ response = requests.post(
+ webhook_url,
+ json=payload,
+ headers={'Content-Type': 'application/json'},
+ timeout=10,
+ )
+ response.raise_for_status()
+ logger.info("Webhook notification sent for message %s to %s", message.id, webhook_url)
+
+
if __name__ == "__main__":
import doctest
diff --git a/rootfs/api/validators.py b/rootfs/api/validators.py
new file mode 100644
index 0000000..b05be8a
--- /dev/null
+++ b/rootfs/api/validators.py
@@ -0,0 +1,19 @@
+
+from django.contrib.auth.validators import UnicodeUsernameValidator
+from django.utils.translation import gettext_lazy as _
+from django.conf import settings
+from django.utils.deconstruct import deconstructible
+
+from api.utils import validate_reserved_names
+
+
+@deconstructible
+class UsernameValidator(UnicodeUsernameValidator):
+ regex = settings.USERNAME_REGEX
+ message = _(
+ f"Enter a valid username. This value may match the regex {regex}."
+ )
+
+ def __call__(self, value):
+ super().__call__(value)
+ validate_reserved_names(value)
diff --git a/rootfs/api/views.py b/rootfs/api/views.py
deleted file mode 100644
index 1f54de2..0000000
--- a/rootfs/api/views.py
+++ /dev/null
@@ -1,304 +0,0 @@
-import logging
-import hashlib
-from django.conf import settings
-from django.core.cache import cache
-from django.utils.decorators import method_decorator
-from django.views.decorators.cache import cache_page
-from django.contrib import messages, auth
-from django.contrib.auth import login
-from django.contrib.auth import views
-from django.contrib.auth import get_user_model
-from django.db.models import Q
-from django.shortcuts import redirect, get_object_or_404, render
-from django.template.loader import render_to_string
-from django.http import HttpResponse, HttpResponseRedirect
-from django.utils.encoding import force_bytes, force_str
-from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
-from django.views.generic import View
-from django.views.generic.edit import CreateView
-from django.utils.translation import gettext_lazy as _
-from django.views.generic.base import TemplateView
-from django.urls import reverse_lazy
-from rest_framework import status
-from rest_framework.exceptions import AuthenticationFailed
-from rest_framework.response import Response
-from rest_framework.viewsets import ModelViewSet
-from oauth2_provider.models import AccessToken
-
-from api import serializers
-from api.forms import AuthenticationForm, RegistrationForm
-from api.exceptions import ServiceUnavailable, DryccException
-from api.utils import token_generator, get_local_host, send_activation_email
-from api.viewset import NormalUserViewSet
-
-User = get_user_model()
-logger = logging.getLogger(__name__)
-
-
-class ReadinessCheckView(View):
- """
- Simple readiness check view to determine DB connection / query.
- """
-
- def get(self, request):
- try:
- import django.db
- with django.db.connection.cursor() as c:
- c.execute("SELECT 0")
- except django.db.Error as e:
- raise ServiceUnavailable("Database health check failed") from e
-
- return HttpResponse("OK")
-
- head = get
-
-
-class LivenessCheckView(View):
- """
- Simple liveness check view to determine if the server
- is responding to HTTP requests.
- """
-
- def get(self, request):
- return HttpResponse("OK")
-
- head = get
-
-
-class RegistrationView(CreateView):
- form_class = RegistrationForm
- template_name = 'user/registration.html'
- success_url = reverse_lazy('user_registration_done')
-
- def get(self, request, *args, **kwargs):
- if settings.LDAP_ENDPOINT or not settings.REGISTRATION_ENABLED:
- return render(request, template_name='user/registration_disable.html')
- return super().get(self, request, *args, **kwargs)
-
- def post(self, request, *args, **kwargs):
- if settings.LDAP_ENDPOINT or not settings.REGISTRATION_ENABLED:
- return render(request, template_name='user/registration_disable.html')
- form = self.form_class(request.POST)
- self.object = None
- if form.is_valid():
- user = form.save(commit=False)
- if settings.EMAIL_HOST:
- user.is_active = False
- user.save()
- send_activation_email(request, user)
- messages.success(request, (
- 'Please Confirm your email to complete registration.'))
- else:
- user.is_active = True
- user.save()
- return self.form_valid(form)
- else:
- return self.form_invalid(form)
-
- def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
- context["h_captcha_key"] = settings.H_CAPTCHA_KEY
- return context
-
-
-class RegistrationDoneView(TemplateView):
- template_name = 'user/registration_done.html'
- title = _('Activate email sent')
-
-
-class ActivateAccount(View):
-
- def get(self, request, uidb64, token, *args, **kwargs):
- try:
- uid = force_str(urlsafe_base64_decode(uidb64))
- user = User.objects.get(pk=uid)
- except (TypeError, ValueError, OverflowError, User.DoesNotExist):
- user = None
- if user is not None and token_generator.check_token(
- user, token):
- user.is_active = True
- user.save()
- login(request, user, backend='django.contrib.auth.backends.ModelBackend')
- messages.success(request, 'Your account have been confirmed.')
- return redirect('/user/activate/done/')
- else:
- messages.warning(request, (
- 'The confirmation link was invalid, possibly because it has already been used.')) # noqa
- return redirect('/user/activate/fail/')
-
-
-class ActivateAccountDoneView(TemplateView):
- template_name = 'user/account_activation_done.html'
- title = _('Activate account done')
-
-
-class ActivateAccountFailView(TemplateView):
- template_name = 'user/account_activation_fail.html'
- title = _('Activate account fail')
-
-
-class UserLoginView(views.LoginView):
- form_class = AuthenticationForm
- extra_context = {
- "registration_enabled": settings.REGISTRATION_ENABLED,
- "password_reset_enabled": True if settings.EMAIL_HOST else False,
- }
- template_name = 'user/login.html'
-
-
-class UserDetailView(NormalUserViewSet):
- serializer_class = serializers.UserSerializer
- required_scopes = ['openid']
-
- def get_object(self):
- return self.request.user
-
- def update(self, request, *args, **kwargs):
- user_serializer = self.serializer_class(
- data=request.data,
- instance=request.user,
- partial=True
- )
- if user_serializer.is_valid():
- if settings.EMAIL_HOST:
- user = self.get_object()
- mail_subject = 'Update your account.'
- uid = urlsafe_base64_encode(force_bytes(user.pk))
- token = token_generator.make_token(user)
- message = render_to_string(
- 'user/account_update_email.html', {
- 'uid': uid,
- 'user': user,
- 'token': token,
- 'domain': get_local_host(request)
- })
- cache_key = "user:serializer:%s" % user.pk
- cache.set(cache_key, request.data, 60 * 30)
- user.email_user(mail_subject, message, fail_silently=True)
- else:
- user_serializer.save()
- return Response(status=status.HTTP_204_NO_CONTENT)
-
-
-class UpdateAccount(View):
-
- def get(self, request, uidb64, token, *args, **kwargs):
- user = get_object_or_404(User, pk=force_str(urlsafe_base64_decode(uidb64)))
- if user is not None and token_generator.check_token(user, token):
- cache_key = "user:serializer:%s" % user.pk
- data = cache.get(cache_key, None)
- if data:
- user_serializer = serializers.UserSerializer(
- data=data, instance=user, partial=True)
- if user_serializer.is_valid():
- user_serializer.save()
- login(request, user, backend='django.contrib.auth.backends.ModelBackend')
- cache.delete(cache_key)
- return render(request, template_name='user/account_update_done.html')
- return render(request, template_name='user/account_update_fail.html')
-
-
-class UserAvatarViewSet(NormalUserViewSet):
-
- @method_decorator(cache_page(60 * 5))
- def avatar(self, request, *args, **kwargs):
- user = User.objects.filter(username=kwargs["username"]).first()
- size = request.GET.get("s", "80")
- md5 = hashlib.md5()
- if user:
- md5.update(user.email.encode("utf8"))
- return HttpResponseRedirect(settings.AVATAR_URL + md5.hexdigest() + "?s=" + size)
-
-
-class UserEmailView(NormalUserViewSet):
- serializer_class = serializers.UserEmailSerializer
- required_scopes = ['openid']
-
- def get_object(self):
- return self.request.user
-
-
-class LoginDoneView(TemplateView):
- template_name = 'user/login_done.html'
-
-
-class UserPasswordResetView(views.PasswordResetView):
- email_template_name = 'user/password_reset_email.html'
- success_url = reverse_lazy('user_password_reset_done')
- template_name = 'user/password_reset_form.html'
-
-
-class UserPasswordResetDoneView(views.PasswordResetDoneView):
- template_name = 'user/password_reset_done.html'
-
-
-class UserPasswordResetConfirmView(views.PasswordResetConfirmView):
- success_url = reverse_lazy('user_password_reset_complete')
- template_name = 'user/password_reset_confirm.html'
-
-
-class UserPasswordResetCompleteView(views.PasswordResetCompleteView):
- template_name = 'user/password_reset_complete.html'
-
-
-class UserLogoutView(views.LogoutView):
- template_name = 'user/logout.html'
-
-
-class ListViewSet(ModelViewSet):
-
- def get_queryset(self, *args, **kwargs):
- serializer = self.serializer_class(data=self.request.query_params)
- serializer.is_valid(raise_exception=True)
-
- serializerlist = serializers.ListSerializer(
- data=self.request.query_params)
- serializerlist.is_valid(raise_exception=True)
- q = Q(user=self.request.user)
- if serializerlist.validated_data.get('section'):
- q &= Q(created__range=serializerlist.validated_data.get('section'))
- return self.model.objects.filter(
- q, **serializer.validated_data).order_by(self.order_by)[0:100]
-
-
-class UserTokensTemplateView(ListViewSet):
- model = AccessToken
- serializer_class = serializers.UserTokensSerializer
- order_by = '-created'
-
- def retrieve(self, request, *args, **kwargs):
- tokens = self.get_queryset(*args, **kwargs)
- serializer = self.get_serializer(tokens, many=True)
- return Response(serializer.data)
-
-
-class UserTokenDeleteView(ListViewSet):
- model = AccessToken
-
- def destroy(self, request, *args, **kwargs):
- token = get_object_or_404(self.model,
- id=self.kwargs['pk'],
- user=request.user)
-
- token.delete()
- return Response(status=status.HTTP_204_NO_CONTENT)
-
-
-class UserAccountPasswordView(ListViewSet):
-
- def update(self, request, *args, **kwargs):
- if settings.LDAP_ENDPOINT:
- raise DryccException(
- "You cannot change user info when ldap is enabled.")
- if not request.data.get('new_password'):
- raise DryccException("new_password is a required field")
- if not request.data.get('password'):
- raise DryccException("password is a required field")
- if len(request.data.get('new_password')) < 8:
- raise DryccException("password must be 8 or more characters. ")
- if not request.user.check_password(request.data['password']):
- raise AuthenticationFailed('Current password does not match')
- request.user.set_password(request.data['new_password'])
- request.user.save()
- auth.logout(request)
- return HttpResponse(status=204)
diff --git a/rootfs/api/views/__init__.py b/rootfs/api/views/__init__.py
new file mode 100644
index 0000000..5d77f0a
--- /dev/null
+++ b/rootfs/api/views/__init__.py
@@ -0,0 +1,2 @@
+from .api import * # noqa
+from .web import * # noqa
diff --git a/rootfs/api/views/api.py b/rootfs/api/views/api.py
new file mode 100644
index 0000000..477e397
--- /dev/null
+++ b/rootfs/api/views/api.py
@@ -0,0 +1,326 @@
+from django.conf import settings
+from django.core.cache import cache
+from django.contrib import auth
+from django.contrib.auth import get_user_model
+from django.db import IntegrityError, connection, Error
+from django.db.models import Q
+from django.shortcuts import get_object_or_404
+from django.template.loader import render_to_string
+from django.http import HttpResponse
+from django.utils.encoding import force_bytes
+from django.utils.http import urlsafe_base64_encode
+from django.urls import reverse_lazy
+from django.views.generic import View
+
+from rest_framework import status, viewsets
+from rest_framework.exceptions import AuthenticationFailed, ValidationError
+from rest_framework.permissions import AllowAny, IsAuthenticated
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from rest_framework.viewsets import GenericViewSet
+from rest_framework.decorators import action
+from rest_framework.mixins import ListModelMixin, DestroyModelMixin
+
+from oauth2_provider.models import AccessToken
+from social_django.models import UserSocialAuth
+
+from api.permissions import HasOAuthScope
+
+from api import serializers
+from api.exceptions import ServiceUnavailable, DryccException
+from api.models import Message, MessagePreference
+from api.utils import get_local_host, get_user_socials, token_generator
+
+
+User = get_user_model()
+
+
+class NormalUserViewSet(viewsets.ModelViewSet):
+ permission_classes = [IsAuthenticated]
+
+
+class ReadinessCheckView(View):
+ """Simple readiness check view to determine DB connection and Migrations."""
+ migrations_completed = False
+
+ def get(self, request):
+ try:
+ with connection.cursor() as c:
+ c.execute("SELECT 0")
+ if not ReadinessCheckView.migrations_completed:
+ from django.db.migrations.executor import MigrationExecutor
+ executor = MigrationExecutor(connection)
+ targets = executor.loader.graph.leaf_nodes()
+ if executor.migration_plan(targets):
+ raise ServiceUnavailable("Migrations are not yet applied")
+ ReadinessCheckView.migrations_completed = True
+ except Error as e:
+ raise ServiceUnavailable(f"Database health check failed: {e}") from e
+ return HttpResponse("OK")
+ head = get
+
+
+class LivenessCheckView(View):
+ """Simple liveness check view to determine if the server is alive."""
+
+ def get(self, request):
+ return HttpResponse("OK")
+
+ head = get
+
+
+class IdentityProviderView(APIView):
+ permission_classes = (AllowAny, )
+
+ def get(self, request, *args, **kwargs):
+ from api.apps_extra.social_core import backends
+
+ results = []
+ for backend_cls in backends.__all__:
+ key_setting = f'SOCIAL_AUTH_{backend_cls.name.upper()}_KEY'
+ secret_setting = f'SOCIAL_AUTH_{backend_cls.name.upper()}_SECRET'
+ if getattr(settings, key_setting, None) and getattr(settings, secret_setting, None):
+ results.append({
+ 'name': backend_cls.name,
+ 'icon': backend_cls.icon,
+ 'login_url': reverse_lazy('social:begin', args=(backend_cls.name,)),
+ })
+
+ return Response({'count': len(results), 'results': results})
+
+
+class UserIdentityViewSet(NormalUserViewSet):
+ serializer_class = serializers.UserEmailSerializer
+ http_method_names = ['get', 'delete']
+
+ def get_queryset(self):
+ return UserSocialAuth.objects.filter(user=self.request.user).order_by('id')
+
+ def list(self, request, *args, **kwargs):
+ results = get_user_socials(request.user)
+ return Response({'count': len(results), 'results': results})
+
+ def destroy(self, request, *args, **kwargs):
+ identity = self.get_object()
+ identity.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+class OAuthPendingView(APIView):
+ permission_classes = (AllowAny, )
+
+ def get(self, request, *args, **kwargs):
+ pending = request.session.get('oauth_pending')
+ if not pending:
+ return Response(
+ {'detail': 'No pending OAuth request'}, status=status.HTTP_404_NOT_FOUND
+ )
+
+ from api.apps_extra.social_core import backends
+
+ backend_map = {backend_cls.name: backend_cls for backend_cls in backends.__all__}
+ backend_cls = backend_map.get(pending.get('provider'))
+ return Response({
+ 'provider': pending.get('provider'),
+ 'email': pending.get('email'),
+ 'icon': backend_cls.icon if backend_cls else '',
+ 'display_name': pending.get('provider', '').title(),
+ })
+
+
+class UserMessageViewSet(NormalUserViewSet):
+ http_method_names = ['get', 'put', 'delete', 'head', 'options']
+ serializer_class = serializers.MessageSerializer
+
+ def get_queryset(self):
+ queryset = Message.objects.filter(user=self.request.user)
+ category = self.request.query_params.get('category')
+ if category and category != 'all':
+ queryset = queryset.filter(category=category)
+ is_read = self.request.query_params.get('is_read')
+ if is_read is not None:
+ queryset = queryset.filter(is_read=is_read.lower() not in ('false', '0', 'no'))
+ search = self.request.query_params.get('search')
+ if search:
+ queryset = queryset.filter(
+ Q(title__icontains=search) | Q(content__icontains=search)
+ )
+ return queryset.order_by('-created_at')
+
+ def retrieve(self, request, *args, **kwargs):
+ message = self.get_object()
+ if not message.is_read:
+ message.is_read = True
+ message.save(update_fields=['is_read'])
+ serializer = self.get_serializer(message)
+ return Response(serializer.data)
+
+ @action(detail=False, methods=['put'], url_path='mark-all-read')
+ def mark_all_read(self, request, *args, **kwargs):
+ self.get_queryset().filter(is_read=False).update(is_read=True)
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+class OAuthCreateUserView(APIView):
+ permission_classes = (AllowAny, )
+
+ def _create_oauth_user(self, request, username, password):
+ pending = request.session.get('oauth_pending')
+ if not pending:
+ raise ValidationError('No pending OAuth request')
+
+ if not username:
+ raise ValidationError('username is a required field')
+ if not password:
+ raise ValidationError('password is a required field')
+ if len(password) < 8:
+ raise ValidationError('password must be 8 or more characters')
+
+ email = pending.get('email')
+ if not email:
+ raise ValidationError('email is missing from oauth provider')
+ if User.objects.filter(email=email).exists():
+ raise ValidationError('email is already registered')
+
+ provider = pending.get('provider')
+ uid = pending.get('uid')
+ extra_data = pending.get('extra_data') or {}
+
+ if not provider or not uid:
+ request.session.pop('oauth_pending', None)
+ raise ValidationError('invalid oauth pending data')
+
+ existing = UserSocialAuth.objects.filter(provider=provider, uid=uid).first()
+ if existing:
+ request.session.pop('oauth_pending', None)
+ raise ValidationError('identity is already linked to another account')
+
+ try:
+ user = User.objects.create_user(username=username, email=email, password=password)
+ except IntegrityError:
+ raise ValidationError('username is already taken')
+
+ UserSocialAuth.objects.create(
+ user=user,
+ provider=provider,
+ uid=uid,
+ extra_data=extra_data,
+ )
+
+ auth.login(request, user, backend='django.contrib.auth.backends.ModelBackend')
+ request.session.pop('oauth_pending', None)
+ return user
+
+ def post(self, request, *args, **kwargs):
+ username = request.data.get('username')
+ password = request.data.get('password')
+ self._create_oauth_user(request, username, password)
+ return Response({'status': 'created'}, status=status.HTTP_201_CREATED)
+
+
+class UserDetailView(NormalUserViewSet):
+ serializer_class = serializers.UserSerializer
+ required_scopes = ['openid']
+ email_template_name = 'user/account_update_email.html'
+
+ def get_object(self):
+ return self.request.user
+
+ def update(self, request, *args, **kwargs):
+ user_serializer = self.serializer_class(
+ data=request.data,
+ instance=request.user,
+ partial=True,
+ )
+ user_serializer.is_valid(raise_exception=True)
+ if getattr(settings, 'EMAIL_HOST', ''):
+ user = self.get_object()
+ mail_subject = 'Update your account.'
+ uid = urlsafe_base64_encode(force_bytes(user.pk))
+ token = token_generator.make_token(user)
+ message = render_to_string(
+ self.email_template_name,
+ {
+ 'uid': uid,
+ 'user': user,
+ 'token': token,
+ 'domain': get_local_host(request),
+ },
+ )
+ cache_key = "user:serializer:%s" % user.pk
+ cache.set(cache_key, request.data, 60 * 30)
+ user.email_user(mail_subject, message, fail_silently=True)
+ else:
+ user_serializer.save()
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+
+class UserEmailView(NormalUserViewSet):
+ serializer_class = serializers.UserEmailSerializer
+ required_scopes = ['openid']
+
+ def get_object(self):
+ return self.request.user
+
+
+class UserTokensView(ListModelMixin, DestroyModelMixin, GenericViewSet):
+ serializer_class = serializers.UserTokensSerializer
+ permission_classes = [IsAuthenticated]
+
+ def get_queryset(self):
+ return AccessToken.objects.filter(user=self.request.user).order_by('-created')
+
+ def get_object(self):
+ return get_object_or_404(self.get_queryset(), id=self.kwargs['pk'])
+
+
+class UserAccountPasswordView(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def update(self, request, *args, **kwargs):
+ if settings.LDAP_ENDPOINT:
+ raise DryccException("You cannot change user info when ldap is enabled.")
+ if not request.data.get('new_password'):
+ raise DryccException("new_password is a required field")
+ if not request.data.get('password'):
+ raise DryccException("password is a required field")
+ if len(request.data.get('new_password')) < 8:
+ raise DryccException("password must be 8 or more characters. ")
+ if not request.user.check_password(request.data['password']):
+ raise AuthenticationFailed('Current password does not match')
+ request.user.set_password(request.data['new_password'])
+ request.user.save()
+ auth.logout(request)
+ return HttpResponse(status=204)
+
+ def put(self, request, *args, **kwargs):
+ return self.update(request, *args, **kwargs)
+
+
+class UserMessagePreferenceViewSet(NormalUserViewSet):
+ serializer_class = serializers.MessagePreferenceSerializer
+ http_method_names = ['get', 'put', 'patch']
+
+ def get_object(self):
+ preference, _ = MessagePreference.objects.get_or_create(user=self.request.user)
+ return preference
+
+ def retrieve(self, request, *args, **kwargs):
+ serializer = self.get_serializer(self.get_object())
+ return Response(serializer.data)
+
+ def update(self, request, *args, **kwargs):
+ partial = kwargs.pop('partial', False)
+ serializer = self.get_serializer(self.get_object(), data=request.data, partial=partial)
+ serializer.is_valid(raise_exception=True)
+ serializer.save()
+ return Response(serializer.data)
+
+
+class ServiceMessageViewSet(viewsets.ModelViewSet):
+ serializer_class = serializers.ServiceMessageSerializer
+ permission_classes = [HasOAuthScope]
+ required_oauth_scopes = ['passport:message']
+
+ def get_queryset(self):
+ return Message.objects.all().order_by('-created_at')
diff --git a/rootfs/api/views/web.py b/rootfs/api/views/web.py
new file mode 100644
index 0000000..b3104f8
--- /dev/null
+++ b/rootfs/api/views/web.py
@@ -0,0 +1,259 @@
+from django.conf import settings
+from django.contrib import messages
+from django.contrib.auth import get_user_model, login, views
+from django.shortcuts import get_object_or_404, redirect, render
+from django.utils.encoding import force_str
+from django.utils.http import urlsafe_base64_decode
+from django.views.generic import View
+from django.views.generic.base import TemplateView
+from django.views.generic.edit import CreateView
+from django.urls import reverse_lazy
+from django.utils.translation import gettext_lazy as _
+
+from rest_framework.exceptions import ValidationError
+from social_django.models import UserSocialAuth
+
+from api import serializers
+from api.forms import AuthenticationForm, RegistrationForm
+from api.utils import get_oauth_callback, send_activation_email, token_generator
+
+
+User = get_user_model()
+
+
+class RegistrationView(CreateView):
+ form_class = RegistrationForm
+ template_name = 'user/registration.html'
+ success_url = reverse_lazy('user_registration_done')
+
+ def get(self, request, *args, **kwargs):
+ if settings.LDAP_ENDPOINT or not settings.REGISTRATION_ENABLED:
+ return render(request, template_name='user/registration_disable.html')
+ return super().get(request, *args, **kwargs)
+
+ def post(self, request, *args, **kwargs):
+ if settings.LDAP_ENDPOINT or not settings.REGISTRATION_ENABLED:
+ return render(request, template_name='user/registration_disable.html')
+ form = self.form_class(request.POST)
+ self.object = None
+ if form.is_valid():
+ user = form.save(commit=False)
+ if getattr(settings, 'EMAIL_HOST', ''):
+ user.is_active = False
+ user.save()
+ send_activation_email(request, user)
+ messages.success(request, 'Please Confirm your email to complete registration.')
+ else:
+ user.is_active = True
+ user.save()
+ return self.form_valid(form)
+ return self.form_invalid(form)
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["h_captcha_key"] = settings.H_CAPTCHA_KEY
+ return context
+
+
+class RegistrationDoneView(TemplateView):
+ template_name = 'user/registration_done.html'
+ title = _('Activate email sent')
+
+
+class ActivateAccount(View):
+
+ def get(self, request, uidb64, token, *args, **kwargs):
+ try:
+ uid = force_str(urlsafe_base64_decode(uidb64))
+ user = User.objects.get(pk=uid)
+ except (TypeError, ValueError, OverflowError, User.DoesNotExist):
+ user = None
+ if user is not None and token_generator.check_token(user, token):
+ user.is_active = True
+ user.save()
+ login(request, user, backend='django.contrib.auth.backends.ModelBackend')
+ messages.success(request, 'Your account have been confirmed.')
+ return redirect('/user/activate/done/')
+
+ messages.warning(
+ request,
+ 'The confirmation link was invalid, possibly because it has already been used.',
+ )
+ return redirect('/user/activate/fail/')
+
+
+class ActivateAccountDoneView(TemplateView):
+ template_name = 'user/account_activation_done.html'
+ title = _('Activate account done')
+
+
+class ActivateAccountFailView(TemplateView):
+ template_name = 'user/account_activation_fail.html'
+ title = _('Activate account fail')
+
+
+class UserLoginView(views.LoginView):
+ form_class = AuthenticationForm
+ template_name = 'user/login.html'
+
+ def _get_identity_linking(self):
+ identity_linking = self.request.POST.get('identity_linking')
+ if identity_linking is None:
+ identity_linking = self.request.GET.get('identity_linking')
+ return identity_linking == '1'
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ identity_linking = self._get_identity_linking()
+ form = kwargs.get('form')
+ context.update({
+ 'identity_linking': identity_linking,
+ 'registration_enabled': settings.REGISTRATION_ENABLED,
+ 'password_reset_enabled': True if getattr(settings, 'EMAIL_HOST', '') else False,
+ 'identity_linking_error': (
+ _('Identity linking login failed. Please try again.')
+ if identity_linking and form is not None and form.errors else None
+ ),
+ })
+ return context
+
+ def form_valid(self, form):
+ response = super().form_valid(form)
+ pending = self.request.session.get('oauth_pending')
+ if not pending:
+ return response
+
+ provider = pending.get('provider')
+ uid = pending.get('uid')
+ extra_data = pending.get('extra_data') or {}
+ if not provider or not uid:
+ self.request.session.pop('oauth_pending', None)
+ raise ValidationError('invalid oauth pending data')
+
+ existing = UserSocialAuth.objects.filter(
+ provider=provider,
+ uid=uid,
+ ).select_related('user').first()
+ if existing and existing.user_id != self.request.user.id:
+ self.request.session.pop('oauth_pending', None)
+ return redirect(get_oauth_callback(self.request, 'conflict', provider))
+
+ if not existing:
+ UserSocialAuth.objects.create(
+ user=self.request.user,
+ provider=provider,
+ uid=uid,
+ extra_data=extra_data,
+ )
+
+ self.request.session.pop('oauth_pending', None)
+ return redirect(get_oauth_callback(self.request, 'linked', provider))
+
+
+class OAuthCallbackTemplateView(TemplateView):
+ template_name = 'user/oauth_callback.html'
+
+ def _build_context(self, request, status_value=None, error=None):
+ pending = request.session.get('oauth_pending')
+ provider = request.GET.get('provider')
+ email = None
+ display_name = None
+
+ if pending:
+ provider = pending.get('provider') or provider
+ email = pending.get('email')
+
+ if provider:
+ display_name = provider.title()
+
+ status_value = status_value or request.GET.get('status') or 'error'
+ if status_value == 'pending' and not pending:
+ status_value = 'error'
+ error = error or 'No pending OAuth request'
+
+ return {
+ 'status': status_value,
+ 'provider': provider,
+ 'display_name': display_name,
+ 'email': email,
+ 'error': error,
+ }
+
+ def get(self, request, *args, **kwargs):
+ context = self._build_context(request)
+ return render(request, self.template_name, context)
+
+ def post(self, request, *args, **kwargs):
+ username = request.POST.get('username')
+ password = request.POST.get('password')
+ confirm_password = request.POST.get('confirm_password')
+
+ if password != confirm_password:
+ context = self._build_context(
+ request,
+ status_value='pending',
+ error='Password confirmation does not match',
+ )
+ return render(request, self.template_name, context)
+
+ from api.views.api import OAuthCreateUserView
+
+ try:
+ OAuthCreateUserView()._create_oauth_user(request, username, password)
+ except ValidationError as exc:
+ error_text = exc.detail
+ if isinstance(error_text, (list, tuple)):
+ error_text = error_text[0]
+ context = self._build_context(request, status_value='pending', error=error_text)
+ return render(request, self.template_name, context)
+
+ return redirect('/account-setting')
+
+
+class UpdateAccount(View):
+ fail_template_name = 'user/account_update_fail.html'
+ success_template_name = 'user/account_update_done.html'
+
+ def get(self, request, uidb64, token, *args, **kwargs):
+ user = get_object_or_404(User, pk=force_str(urlsafe_base64_decode(uidb64)))
+ if user is not None and token_generator.check_token(user, token):
+ cache_key = "user:serializer:%s" % user.pk
+ from django.core.cache import cache
+ data = cache.get(cache_key, None)
+ if data:
+ user_serializer = serializers.UserSerializer(
+ data=data, instance=user, partial=True
+ )
+ if user_serializer.is_valid():
+ user_serializer.save()
+ login(request, user, backend='django.contrib.auth.backends.ModelBackend')
+ cache.delete(cache_key)
+ return render(request, template_name=self.success_template_name)
+ return render(request, template_name=self.fail_template_name)
+
+
+class LoginDoneView(TemplateView):
+ template_name = 'user/login_done.html'
+
+
+class UserPasswordResetView(views.PasswordResetView):
+ email_template_name = 'user/password_reset_email.html'
+ success_url = reverse_lazy('user_password_reset_done')
+ template_name = 'user/password_reset_form.html'
+
+
+class UserPasswordResetDoneView(views.PasswordResetDoneView):
+ template_name = 'user/password_reset_done.html'
+
+
+class UserPasswordResetConfirmView(views.PasswordResetConfirmView):
+ success_url = reverse_lazy('user_password_reset_complete')
+ template_name = 'user/password_reset_confirm.html'
+
+
+class UserPasswordResetCompleteView(views.PasswordResetCompleteView):
+ template_name = 'user/password_reset_complete.html'
+
+
+class UserLogoutView(views.LogoutView):
+ template_name = 'user/logout.html'
diff --git a/rootfs/api/viewset.py b/rootfs/api/viewset.py
deleted file mode 100644
index 9ccba36..0000000
--- a/rootfs/api/viewset.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from rest_framework import viewsets
-from rest_framework.permissions import IsAuthenticated
-
-
-class NormalUserViewSet(viewsets.ModelViewSet):
- """
- A simple ViewSet for objects filtered by their 'owner' attribute.
-
- To use it, at minimum you'll need to provide the `serializer_class` attribute and
- the `model` attribute shortcut.
- """
- permission_classes = [IsAuthenticated]
diff --git a/rootfs/bin/test-unit b/rootfs/bin/test-unit
index b586069..de37e50 100755
--- a/rootfs/bin/test-unit
+++ b/rootfs/bin/test-unit
@@ -6,13 +6,19 @@
# fail hard and fast even on pipelines
set -eou pipefail
+function start_valkey() {
+ cd /tmp
+ nohup valkey-server > /var/log/valkey.log 2>&1 &
+ cd -
+}
+
function creating_rsa_key() {
mkdir -p /var/run/secrets/drycc/passport/ \
&& openssl genrsa -out /var/run/secrets/drycc/passport/oidc-rsa-private-key 4096
}
gosu postgres pg_ctl -D "$PGDATA" start && ln -s /tmp/.s.PGSQL.5432 /var/run/postgresql/.s.PGSQL.5432
+start_valkey
creating_rsa_key
python3 manage.py check
coverage run manage.py test --settings=api.settings.testing --noinput api
-coverage report -m
diff --git a/rootfs/bin/upload-coverage b/rootfs/bin/upload-coverage
index 53433f3..492452b 100755
--- a/rootfs/bin/upload-coverage
+++ b/rootfs/bin/upload-coverage
@@ -6,4 +6,8 @@
# fail hard and fast even on pipelines
set -eou pipefail
-codecov --required
+coverage report -m > coverage.txt
+
+if [[ -n $CODECOV_TOKEN ]]; then
+ codecov upload-process --plugin=noop -t "$CODECOV_TOKEN"
+fi
diff --git a/rootfs/dev_requirements.txt b/rootfs/dev_requirements.txt
index 6c42313..ef0abcb 100644
--- a/rootfs/dev_requirements.txt
+++ b/rootfs/dev_requirements.txt
@@ -1,13 +1,10 @@
# test module
# test
# Run "make test-unit" for the % of code exercised during tests
-coverage==7.4.4
+coverage==7.10.6
# Run "make test-style" to check python syntax and style
-flake8==7.0.0
-
-# code coverage report at https://codecov.io/github/drycc/passport
-codecov==2.1.13
+flake8==7.3.0
# mock out python-requests, mostly k8s
-requests-mock==1.11.0
+requests-mock==1.12.1
diff --git a/rootfs/passport/urls.py b/rootfs/passport/urls.py
index 6740132..a3a0dbf 100644
--- a/rootfs/passport/urls.py
+++ b/rootfs/passport/urls.py
@@ -18,7 +18,6 @@
from django.contrib import admin
from django.views.generic.base import TemplateView
from api.views import LivenessCheckView, ReadinessCheckView
-from . import views
if settings.ADMIN_ENABLED:
urlpatterns = [path('admin/', admin.site.urls)]
@@ -28,9 +27,9 @@
urlpatterns += [
re_path(r'^healthz$', LivenessCheckView.as_view()),
re_path(r'^readiness$', ReadinessCheckView.as_view()),
- re_path(r"settings/?$", views.SettingsViewSet.as_view({'get': 'retrieve'})),
- re_path(r"^user/", include('api.urls')),
+ re_path(r"^", include('api.urls')),
re_path(r'^oauth/', include('oauth2_provider.urls', namespace='oauth2_provider')),
re_path(r'^accounts/', include('django.contrib.auth.urls')),
+ re_path(r'^auth/', include('social_django.urls', namespace='social')),
re_path(r'.*', TemplateView.as_view(template_name="index.html")),
]
diff --git a/rootfs/passport/views.py b/rootfs/passport/views.py
index 42f9493..ede1bf9 100644
--- a/rootfs/passport/views.py
+++ b/rootfs/passport/views.py
@@ -4,12 +4,17 @@
from rest_framework.response import Response
-class SettingsViewSet(GenericViewSet):
+class AppSettingsViewSet(GenericViewSet):
permission_classes = (AllowAny, )
def retrieve(self, request, *args, **kwargs):
return Response(data={
- "legal": settings.LEGAL_ENABLED,
- "registration_enabled": settings.REGISTRATION_ENABLED,
+ "legal": getattr(settings, 'LEGAL_ENABLED', False),
+ "dashboard_url": getattr(settings, 'DASHBOARD_URL', '/'),
+ "contact_support_url": getattr(
+ settings, 'CONTACT_SUPPORT_URL',
+ 'https://community.drycc.cc/'
+ ),
+ "registration_enabled": getattr(settings, 'REGISTRATION_ENABLED', False),
})
diff --git a/rootfs/requirements.txt b/rootfs/requirements.txt
index 482fd7f..2b6f940 100644
--- a/rootfs/requirements.txt
+++ b/rootfs/requirements.txt
@@ -1,16 +1,19 @@
# Drycc passport requirements
-django==4.2.11
-pytz==2024.1
-django-auth-ldap==4.6.0
-django-cors-headers==4.3.1
-djangorestframework==3.15.1
-gunicorn==22.0.0
-uvicorn==0.29.0
-asgiref==3.8.0
-psycopg[binary]==3.1.18
-requests==2.31.0
+django==5.2.14
+pytz==2026.1
+django-auth-ldap==5.3.0
+django-cors-headers==4.9.0
+djangorestframework==3.17.0
+gunicorn==25.1.0
+uvicorn==0.42.0
+asgiref==3.11.1
+psycopg[binary]==3.3.3
+requests==2.33.0
requests-toolbelt==1.0.0
-django_redis==5.4.0
-dj-database-url==2.1.0
-django-oauth-toolkit==2.3.0
-whitenoise==6.6.0
+redis==6.3.0
+celery[redis]==5.6.3
+hiredis==3.3.1
+dj-database-url==3.1.2
+django-oauth-toolkit==3.2.0
+social-auth-app-django==5.7.0
+whitenoise==6.12.0
diff --git a/rootfs/web/README.md b/rootfs/web/README.md
new file mode 100644
index 0000000..3d51350
--- /dev/null
+++ b/rootfs/web/README.md
@@ -0,0 +1,83 @@
+# Passport Web Frontend
+
+This is the web frontend for the Passport authentication system, built with Vue.js and Vite.
+
+## Prerequisites
+
+Before running the web frontend, ensure you have the following installed:
+- Node.js (v23 or higher)
+- npm or yarn
+- Python environment with Django backend
+
+## Setup Instructions
+
+### 1. Backend Setup
+
+First, ensure the Django backend is properly set up:
+
+```bash
+# Install Django and dependencies
+pip install -r requirements.txt
+
+# Set environment variables
+export DRYCC_DATABASE_URL=postgres://postgres:@127.0.0.1:5432/passport
+export VUE_APP_BASE_URL=http://localhost:8000/
+export CSRF_TRUSTED_ORIGINS=http://localhost:5173
+
+# Start the Django development server
+python manage.py runserver
+```
+
+### 2. Frontend Setup
+
+Navigate to the web directory and install dependencies:
+
+```bash
+cd web
+npm install
+```
+
+### 3. Development Server
+
+Start the development server:
+
+```bash
+npm run dev -- --port 5173
+```
+
+The application will be available at `http://localhost:5173`.
+
+## Available Scripts
+
+- `npm run dev` - Start the development server
+- `npm run build` - Build the application for production
+- `npm run preview` - Preview the production build
+- `npm run lint` - Run ESLint
+
+## Environment Variables
+
+The following environment variables are required:
+
+- `VUE_APP_BASE_URL` - The base URL of the Django backend API
+- `CSRF_TRUSTED_ORIGINS` - Trusted origins for CSRF protection
+
+## Project Structure
+
+```
+web/
+├── public/ # Static assets
+├── src/
+│ ├── assets/ # Images and other assets
+│ ├── components/ # Vue components
+│ ├── views/ # Page components
+│ ├── services/ # API service modules
+│ ├── router/ # Vue Router configuration
+│ ├── utils/ # Utility functions
+│ └── lang/ # Internationalization
+├── package.json # Project dependencies
+└── vite.config.js # Vite configuration
+```
+
+## Contributing
+
+Please refer to the main project documentation for contribution guidelines.
diff --git a/rootfs/web/index.html b/rootfs/web/index.html
index 7c24d84..369b5fa 100644
--- a/rootfs/web/index.html
+++ b/rootfs/web/index.html
@@ -2,17 +2,11 @@
-
- Drycc Passport
+ Drycc CaaS
-
-
+
-
+
diff --git a/rootfs/web/package-lock.json b/rootfs/web/package-lock.json
index 798ffc0..41980a8 100644
--- a/rootfs/web/package-lock.json
+++ b/rootfs/web/package-lock.json
@@ -1,1496 +1,2371 @@
{
"name": "web",
"version": "0.0.0",
- "lockfileVersion": 2,
+ "lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "web",
"version": "0.0.0",
+ "license": "Apache-2.0",
"dependencies": {
- "@icon-park/vue-next": "^1.3.6",
- "axios": "^0.26.1",
- "vant": "^3.4.5",
- "vue": "^3.2.31",
- "vue-i18n": "^9.1.9",
- "vue-router": "^4.0.14",
- "yarn": "^1.22.17"
+ "axios": "^1.15.2",
+ "vue": "^3.5.30",
+ "vue-i18n": "^11.3.0",
+ "vue-router": "^5.0.4",
+ "yarn": "^1.22.22"
},
"devDependencies": {
- "@vitejs/plugin-vue": "^2.2.4",
- "@vue/compiler-sfc": "^3.2.31",
- "vite": "^2.8.6"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.16.7",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.7.tgz",
- "integrity": "sha512-sR4eaSrnM7BV7QPzGfEX5paG/6wrZM3I0HDzfIAK06ESvo9oy3xBuVBxE3MbQaKNhvg8g/ixjMWo2CGpzpHsDA==",
- "bin": {
- "parser": "bin/babel-parser.js"
+ "@tailwindcss/vite": "^4.2.2",
+ "@vitejs/plugin-vue": "^6.0.5",
+ "@vue/compiler-sfc": "^3.5.30",
+ "lucide-vue-next": "^1.0.0",
+ "tailwindcss": "^4.2.2",
+ "typescript": "^6.0.3",
+ "vite": "^8.0.8",
+ "vue-tsc": "^3.2.6"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
},
"engines": {
- "node": ">=6.0.0"
+ "node": ">=6.9.0"
}
},
- "node_modules/@icon-park/vue-next": {
- "version": "1.3.6",
- "resolved": "https://registry.npmjs.org/@icon-park/vue-next/-/vue-next-1.3.6.tgz",
- "integrity": "sha512-AZaCcjRPU9vTNVfcrG03CYJM+uv0SXx5lC4njanlRorFo+TV/x0ZTYGYGpTR/l6ek6QmFu9THmrgqKl7i/8yHg==",
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
"engines": {
- "node": ">= 8.0.0",
- "npm": ">= 5.0.0"
- },
- "peerDependencies": {
- "vue": "3.x"
+ "node": ">=6.9.0"
}
},
- "node_modules/@intlify/core-base": {
- "version": "9.1.9",
- "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.9.tgz",
- "integrity": "sha512-x5T0p/Ja0S8hs5xs+ImKyYckVkL4CzcEXykVYYV6rcbXxJTe2o58IquSqX9bdncVKbRZP7GlBU1EcRaQEEJ+vw==",
- "dependencies": {
- "@intlify/devtools-if": "9.1.9",
- "@intlify/message-compiler": "9.1.9",
- "@intlify/message-resolver": "9.1.9",
- "@intlify/runtime": "9.1.9",
- "@intlify/shared": "9.1.9",
- "@intlify/vue-devtools": "9.1.9"
- },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
"engines": {
- "node": ">= 10"
+ "node": ">=6.9.0"
}
},
- "node_modules/@intlify/devtools-if": {
- "version": "9.1.9",
- "resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.1.9.tgz",
- "integrity": "sha512-oKSMKjttG3Ut/1UGEZjSdghuP3fwA15zpDPcjkf/1FjlOIm6uIBGMNS5jXzsZy593u+P/YcnrZD6cD3IVFz9vQ==",
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "license": "MIT",
"dependencies": {
- "@intlify/shared": "9.1.9"
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
},
"engines": {
- "node": ">= 10"
+ "node": ">=6.0.0"
}
},
- "node_modules/@intlify/message-compiler": {
- "version": "9.1.9",
- "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.1.9.tgz",
- "integrity": "sha512-6YgCMF46Xd0IH2hMRLCssZI3gFG4aywidoWQ3QP4RGYQXQYYfFC54DxhSgfIPpVoPLQ+4AD29eoYmhiHZ+qLFQ==",
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "license": "MIT",
"dependencies": {
- "@intlify/message-resolver": "9.1.9",
- "@intlify/shared": "9.1.9",
- "source-map": "0.6.1"
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
- "node": ">= 10"
+ "node": ">=6.9.0"
}
},
- "node_modules/@intlify/message-resolver": {
- "version": "9.1.9",
- "resolved": "https://registry.npmjs.org/@intlify/message-resolver/-/message-resolver-9.1.9.tgz",
- "integrity": "sha512-Lx/DBpigeK0sz2BBbzv5mu9/dAlt98HxwbG7xLawC3O2xMF9MNWU5FtOziwYG6TDIjNq0O/3ZbOJAxwITIWXEA==",
- "engines": {
- "node": ">= 10"
+ "node_modules/@emnapi/core": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz",
+ "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.1",
+ "tslib": "^2.4.0"
}
},
- "node_modules/@intlify/runtime": {
- "version": "9.1.9",
- "resolved": "https://registry.npmjs.org/@intlify/runtime/-/runtime-9.1.9.tgz",
- "integrity": "sha512-XgPw8+UlHCiie3fI41HPVa/VDJb3/aSH7bLhY1hJvlvNV713PFtb4p4Jo+rlE0gAoMsMCGcsiT982fImolSltg==",
+ "node_modules/@emnapi/runtime": {
+ "version": "1.9.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz",
+ "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
"dependencies": {
- "@intlify/message-compiler": "9.1.9",
- "@intlify/message-resolver": "9.1.9",
- "@intlify/shared": "9.1.9"
- },
- "engines": {
- "node": ">= 10"
+ "tslib": "^2.4.0"
}
},
- "node_modules/@intlify/shared": {
- "version": "9.1.9",
- "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.1.9.tgz",
- "integrity": "sha512-xKGM1d0EAxdDFCWedcYXOm6V5Pfw/TMudd6/qCdEb4tv0hk9EKeg7lwQF1azE0dP2phvx0yXxrt7UQK+IZjNdw==",
- "engines": {
- "node": ">= 10"
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
}
},
- "node_modules/@intlify/vue-devtools": {
- "version": "9.1.9",
- "resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.1.9.tgz",
- "integrity": "sha512-YPehH9uL4vZcGXky4Ev5qQIITnHKIvsD2GKGXgqf+05osMUI6WSEQHaN9USRa318Rs8RyyPCiDfmA0hRu3k7og==",
+ "node_modules/@intlify/core-base": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.3.0.tgz",
+ "integrity": "sha512-NNX5jIwF4TJBe7RtSKDMOA6JD9mp2mRcBHAwt2X+Q8PvnZub0yj5YYXlFu2AcESdgQpEv/5Yx2uOCV/yh7YkZg==",
+ "license": "MIT",
"dependencies": {
- "@intlify/message-resolver": "9.1.9",
- "@intlify/runtime": "9.1.9",
- "@intlify/shared": "9.1.9"
+ "@intlify/devtools-types": "11.3.0",
+ "@intlify/message-compiler": "11.3.0",
+ "@intlify/shared": "11.3.0"
},
"engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@popperjs/core": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz",
- "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==",
+ "node": ">= 16"
+ },
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/popperjs"
+ "url": "https://github.com/sponsors/kazupon"
}
},
- "node_modules/@vant/icons": {
- "version": "1.7.3",
- "resolved": "https://registry.npmjs.org/@vant/icons/-/icons-1.7.3.tgz",
- "integrity": "sha512-tW4EqzxN4kXw1rnlnQJQHofEifPbt/gECOWiibomht8QLyvoGuE4iUmDFS288dJ07ZjuTy0bhdABj0SENo2fmQ=="
- },
- "node_modules/@vant/popperjs": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@vant/popperjs/-/popperjs-1.1.0.tgz",
- "integrity": "sha512-8MD1gz146awV/uPxYjz4pet22f7a9YVKqk7T+gFkWFwT9mEcrIUEg/xPrdOnWKLP9puXyYtm7oVfSDSefZ/p/w==",
+ "node_modules/@intlify/devtools-types": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.3.0.tgz",
+ "integrity": "sha512-G9CNL4WpANWVdUjubOIIS7/D2j/0j+1KJmhBJxHilWNKr9mmt3IjFV3Hq4JoBP23uOoC5ynxz/FHZ42M+YxfGw==",
+ "license": "MIT",
"dependencies": {
- "@popperjs/core": "^2.9.2"
- }
- },
- "node_modules/@vant/use": {
- "version": "1.3.6",
- "resolved": "https://registry.npmjs.org/@vant/use/-/use-1.3.6.tgz",
- "integrity": "sha512-3z+nywPaV2F5BdJO7RQxWlgfzJeEOmViD2yHMb7Tg+R4NR/7iQskqW8v2Cnv9FWSJgTOSHlcr7UzeLpiTAP4HA=="
- },
- "node_modules/@vitejs/plugin-vue": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.2.4.tgz",
- "integrity": "sha512-ev9AOlp0ljCaDkFZF3JwC/pD2N4Hh+r5srl5JHM6BKg5+99jiiK0rE/XaRs3pVm1wzyKkjUy/StBSoXX5fFzcw==",
- "dev": true,
+ "@intlify/core-base": "11.3.0",
+ "@intlify/shared": "11.3.0"
+ },
"engines": {
- "node": ">=12.0.0"
+ "node": ">= 16"
},
- "peerDependencies": {
- "vite": "^2.5.10",
- "vue": "^3.2.25"
- }
- },
- "node_modules/@vue/compiler-core": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.31.tgz",
- "integrity": "sha512-aKno00qoA4o+V/kR6i/pE+aP+esng5siNAVQ422TkBNM6qA4veXiZbSe8OTXHXquEi/f6Akc+nLfB4JGfe4/WQ==",
- "dependencies": {
- "@babel/parser": "^7.16.4",
- "@vue/shared": "3.2.31",
- "estree-walker": "^2.0.2",
- "source-map": "^0.6.1"
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
}
},
- "node_modules/@vue/compiler-dom": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.31.tgz",
- "integrity": "sha512-60zIlFfzIDf3u91cqfqy9KhCKIJgPeqxgveH2L+87RcGU/alT6BRrk5JtUso0OibH3O7NXuNOQ0cDc9beT0wrg==",
+ "node_modules/@intlify/message-compiler": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.3.0.tgz",
+ "integrity": "sha512-RAJp3TMsqohg/Wa7bVF3cChRhecSYBLrTCQSj7j0UtWVFLP+6iEJoE2zb7GU5fp+fmG5kCbUdzhmlAUCWXiUJw==",
+ "license": "MIT",
"dependencies": {
- "@vue/compiler-core": "3.2.31",
- "@vue/shared": "3.2.31"
+ "@intlify/shared": "11.3.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
}
},
- "node_modules/@vue/compiler-sfc": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.31.tgz",
- "integrity": "sha512-748adc9msSPGzXgibHiO6T7RWgfnDcVQD+VVwYgSsyyY8Ans64tALHZANrKtOzvkwznV/F4H7OAod/jIlp/dkQ==",
- "dependencies": {
- "@babel/parser": "^7.16.4",
- "@vue/compiler-core": "3.2.31",
- "@vue/compiler-dom": "3.2.31",
- "@vue/compiler-ssr": "3.2.31",
- "@vue/reactivity-transform": "3.2.31",
- "@vue/shared": "3.2.31",
- "estree-walker": "^2.0.2",
- "magic-string": "^0.25.7",
- "postcss": "^8.1.10",
- "source-map": "^0.6.1"
+ "node_modules/@intlify/shared": {
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.3.0.tgz",
+ "integrity": "sha512-LC6P/uay7rXL5zZ5+5iRJfLs/iUN8apu9tm8YqQVmW3Uq3X4A0dOFUIDuAmB7gAC29wTHOS3EiN/IosNSz0eNQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
}
},
- "node_modules/@vue/compiler-ssr": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.31.tgz",
- "integrity": "sha512-mjN0rqig+A8TVDnsGPYJM5dpbjlXeHUm2oZHZwGyMYiGT/F4fhJf/cXy8QpjnLQK4Y9Et4GWzHn9PS8AHUnSkw==",
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.2.31",
- "@vue/shared": "3.2.31"
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
}
},
- "node_modules/@vue/devtools-api": {
- "version": "6.0.13",
- "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.13.tgz",
- "integrity": "sha512-T34EjcArVqzANedEZe2kXQ+AZsld2z1ptJlkOGm87+blk+s6udnP4ze/NYqV8lz1o9AIivimN0xxteLlWiWQdg=="
- },
- "node_modules/@vue/reactivity": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.31.tgz",
- "integrity": "sha512-HVr0l211gbhpEKYr2hYe7hRsV91uIVGFYNHj73njbARVGHQvIojkImKMaZNDdoDZOIkMsBc9a1sMqR+WZwfSCw==",
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
"dependencies": {
- "@vue/shared": "3.2.31"
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
}
},
- "node_modules/@vue/reactivity-transform": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.31.tgz",
- "integrity": "sha512-uS4l4z/W7wXdI+Va5pgVxBJ345wyGFKvpPYtdSgvfJfX/x2Ymm6ophQlXXB6acqGHtXuBqNyyO3zVp9b1r0MOA==",
- "dependencies": {
- "@babel/parser": "^7.16.4",
- "@vue/compiler-core": "3.2.31",
- "@vue/shared": "3.2.31",
- "estree-walker": "^2.0.2",
- "magic-string": "^0.25.7"
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
}
},
- "node_modules/@vue/runtime-core": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.31.tgz",
- "integrity": "sha512-Kcog5XmSY7VHFEMuk4+Gap8gUssYMZ2+w+cmGI6OpZWYOEIcbE0TPzzPHi+8XTzAgx1w/ZxDFcXhZeXN5eKWsA==",
- "dependencies": {
- "@vue/reactivity": "3.2.31",
- "@vue/shared": "3.2.31"
- }
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
},
- "node_modules/@vue/runtime-dom": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.31.tgz",
- "integrity": "sha512-N+o0sICVLScUjfLG7u9u5XCjvmsexAiPt17GNnaWHJUfsKed5e85/A3SWgKxzlxx2SW/Hw7RQxzxbXez9PtY3g==",
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
"dependencies": {
- "@vue/runtime-core": "3.2.31",
- "@vue/shared": "3.2.31",
- "csstype": "^2.6.8"
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
}
},
- "node_modules/@vue/server-renderer": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.31.tgz",
- "integrity": "sha512-8CN3Zj2HyR2LQQBHZ61HexF5NReqngLT3oahyiVRfSSvak+oAvVmu8iNLSu6XR77Ili2AOpnAt1y8ywjjqtmkg==",
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz",
+ "integrity": "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
"dependencies": {
- "@vue/compiler-ssr": "3.2.31",
- "@vue/shared": "3.2.31"
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
},
"peerDependencies": {
- "vue": "3.2.31"
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
}
},
- "node_modules/@vue/shared": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz",
- "integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ=="
- },
- "node_modules/axios": {
- "version": "0.26.1",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
- "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
- "dependencies": {
- "follow-redirects": "^1.14.8"
+ "node_modules/@oxc-project/types": {
+ "version": "0.124.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz",
+ "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
}
},
- "node_modules/csstype": {
- "version": "2.6.20",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
- "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
- },
- "node_modules/esbuild": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.25.tgz",
- "integrity": "sha512-4JHEIOMNFvK09ziiL+iVmldIhLbn49V4NAVo888tcGFKedEZY/Y8YapfStJ6zSE23tzYPKxqKwQBnQoIO0BI/Q==",
- "dev": true,
- "hasInstallScript": true,
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=12"
- },
- "optionalDependencies": {
- "esbuild-android-64": "0.14.25",
- "esbuild-android-arm64": "0.14.25",
- "esbuild-darwin-64": "0.14.25",
- "esbuild-darwin-arm64": "0.14.25",
- "esbuild-freebsd-64": "0.14.25",
- "esbuild-freebsd-arm64": "0.14.25",
- "esbuild-linux-32": "0.14.25",
- "esbuild-linux-64": "0.14.25",
- "esbuild-linux-arm": "0.14.25",
- "esbuild-linux-arm64": "0.14.25",
- "esbuild-linux-mips64le": "0.14.25",
- "esbuild-linux-ppc64le": "0.14.25",
- "esbuild-linux-riscv64": "0.14.25",
- "esbuild-linux-s390x": "0.14.25",
- "esbuild-netbsd-64": "0.14.25",
- "esbuild-openbsd-64": "0.14.25",
- "esbuild-sunos-64": "0.14.25",
- "esbuild-windows-32": "0.14.25",
- "esbuild-windows-64": "0.14.25",
- "esbuild-windows-arm64": "0.14.25"
- }
- },
- "node_modules/esbuild-android-64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.25.tgz",
- "integrity": "sha512-L5vCUk7TzFbBnoESNoXjU3x9+/+7TDIE/1mTfy/erAfvZAqC+S3sp/Qa9wkypFMcFvN9FzvESkTlpeQDolREtQ==",
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz",
+ "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==",
"cpu": [
- "x64"
+ "arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/esbuild-android-arm64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.25.tgz",
- "integrity": "sha512-4jv5xPjM/qNm27T5j3ZEck0PvjgQtoMHnz4FzwF5zNP56PvY2CT0WStcAIl6jNlsuDdN63rk2HRBIsO6xFbcFw==",
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz",
+ "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "android"
+ "darwin"
],
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/esbuild-darwin-64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.25.tgz",
- "integrity": "sha512-TGp8tuudIxOyWd1+8aYPxQmC1ZQyvij/AfNBa35RubixD0zJ1vkKHVAzo0Zao1zcG6pNqiSyzfPto8vmg0s7oA==",
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz",
+ "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/esbuild-darwin-arm64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.25.tgz",
- "integrity": "sha512-oTcDgdm0MDVEmw2DWu8BV68pYuImpFgvWREPErBZmNA4MYKGuBRaCiJqq6jZmBR1x+3y1DWCjez+5uLtuAm6mw==",
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz",
+ "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==",
"cpu": [
- "arm64"
+ "x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "darwin"
+ "freebsd"
],
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/esbuild-freebsd-64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.25.tgz",
- "integrity": "sha512-ueAqbnMZ8arnuLH8tHwTCQYeptnHOUV7vA6px6j4zjjQwDx7TdP7kACPf3TLZLdJQ3CAD1XCvQ2sPhX+8tacvQ==",
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz",
+ "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==",
"cpu": [
- "x64"
+ "arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "freebsd"
+ "linux"
],
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/esbuild-freebsd-arm64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.25.tgz",
- "integrity": "sha512-+ZVWud2HKh+Ob6k/qiJWjBtUg4KmJGGmbvEXXW1SNKS7hW7HU+Zq2ZCcE1akFxOPkVB+EhOty/sSek30tkCYug==",
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz",
+ "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "freebsd"
+ "linux"
],
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/esbuild-linux-32": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.25.tgz",
- "integrity": "sha512-3OP/lwV3kCzEz45tobH9nj+uE4ubhGsfx+tn0L26WAGtUbmmcRpqy7XRG/qK7h1mClZ+eguIANcQntYMdYklfw==",
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz",
+ "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==",
"cpu": [
- "ia32"
+ "arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/esbuild-linux-64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.25.tgz",
- "integrity": "sha512-+aKHdHZmX9qwVlQmu5xYXh7GsBFf4TWrePgeJTalhXHOG7NNuUwoHmketGiZEoNsWyyqwH9rE5BC+iwcLY30Ug==",
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz",
+ "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==",
"cpu": [
- "x64"
+ "ppc64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/esbuild-linux-arm": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.25.tgz",
- "integrity": "sha512-aTLcE2VBoLydL943REcAcgnDi3bHtmULSXWLbjtBdtykRatJVSxKMjK9YlBXUZC4/YcNQfH7AxwVeQr9fNxPhw==",
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz",
+ "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==",
"cpu": [
- "arm"
+ "s390x"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/esbuild-linux-arm64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.25.tgz",
- "integrity": "sha512-UxfenPx/wSZx55gScCImPtXekvZQLI2GW3qe5dtlmU7luiqhp5GWPzGeQEbD3yN3xg/pHc671m5bma5Ns7lBHw==",
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz",
+ "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==",
"cpu": [
- "arm64"
+ "x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/esbuild-linux-mips64le": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.25.tgz",
- "integrity": "sha512-wLWYyqVfYx9Ur6eU5RT92yJVsaBGi5RdkoWqRHOqcJ38Kn60QMlcghsKeWfe9jcYut8LangYZ98xO1LxIoSXrQ==",
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz",
+ "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==",
"cpu": [
- "mips64el"
+ "x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/esbuild-linux-ppc64le": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.25.tgz",
- "integrity": "sha512-0dR6Csl6Zas3g4p9ULckEl8Mo8IInJh33VCJ3eaV1hj9+MHGdmDOakYMN8MZP9/5nl+NU/0ygpd14cWgy8uqRw==",
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz",
+ "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==",
"cpu": [
- "ppc64"
+ "arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "linux"
+ "openharmony"
],
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/esbuild-linux-riscv64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.25.tgz",
- "integrity": "sha512-J4d20HDmTrgvhR0bdkDhvvJGaikH3LzXQnNaseo8rcw9Yqby9A90gKUmWpfwqLVNRILvNnAmKLfBjCKU9ajg8w==",
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz",
+ "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==",
"cpu": [
- "riscv64"
+ "wasm32"
],
"dev": true,
+ "license": "MIT",
"optional": true,
- "os": [
- "linux"
- ],
+ "dependencies": {
+ "@emnapi/core": "1.9.2",
+ "@emnapi/runtime": "1.9.2",
+ "@napi-rs/wasm-runtime": "^1.1.3"
+ },
"engines": {
- "node": ">=12"
+ "node": ">=14.0.0"
}
},
- "node_modules/esbuild-linux-s390x": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.25.tgz",
- "integrity": "sha512-YI2d5V6nTE73ZnhEKQD7MtsPs1EtUZJ3obS21oxQxGbbRw1G+PtJKjNyur+3t6nzHP9oTg6GHQ3S3hOLLmbDIQ==",
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz",
+ "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==",
"cpu": [
- "s390x"
+ "arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "linux"
+ "win32"
],
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/esbuild-netbsd-64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.25.tgz",
- "integrity": "sha512-TKIVgNWLUOkr+Exrye70XTEE1lJjdQXdM4tAXRzfHE9iBA7LXWcNtVIuSnphTqpanPzTDFarF0yqq4kpbC6miA==",
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz",
+ "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "netbsd"
+ "win32"
],
"engines": {
- "node": ">=12"
+ "node": "^20.19.0 || >=22.12.0"
}
},
- "node_modules/esbuild-openbsd-64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.25.tgz",
- "integrity": "sha512-QgFJ37A15D7NIXBTYEqz29+uw3nNBOIyog+3kFidANn6kjw0GHZ0lEYQn+cwjyzu94WobR+fes7cTl/ZYlHb1A==",
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz",
+ "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
+ "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.19.0",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.32.0",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.2.2"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz",
+ "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.2.2",
+ "@tailwindcss/oxide-darwin-arm64": "4.2.2",
+ "@tailwindcss/oxide-darwin-x64": "4.2.2",
+ "@tailwindcss/oxide-freebsd-x64": "4.2.2",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.2.2",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.2.2",
+ "@tailwindcss/oxide-linux-x64-musl": "4.2.2",
+ "@tailwindcss/oxide-wasm32-wasi": "4.2.2",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.2.2"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz",
+ "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==",
"cpu": [
- "x64"
+ "arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "openbsd"
+ "android"
],
"engines": {
- "node": ">=12"
+ "node": ">= 20"
}
},
- "node_modules/esbuild-sunos-64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.25.tgz",
- "integrity": "sha512-rmWfjUItYIVlqr5EnTH1+GCxXiBOC42WBZ3w++qh7n2cS9Xo0lO5pGSG2N+huOU2fX5L+6YUuJ78/vOYvefeFw==",
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz",
+ "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==",
"cpu": [
- "x64"
+ "arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "sunos"
+ "darwin"
],
"engines": {
- "node": ">=12"
+ "node": ">= 20"
}
},
- "node_modules/esbuild-windows-32": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.25.tgz",
- "integrity": "sha512-HGAxVUofl3iUIz9W10Y9XKtD0bNsK9fBXv1D55N/ljNvkrAYcGB8YCm0v7DjlwtyS6ws3dkdQyXadbxkbzaKOA==",
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz",
+ "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==",
"cpu": [
- "ia32"
+ "x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "win32"
+ "darwin"
],
"engines": {
- "node": ">=12"
+ "node": ">= 20"
}
},
- "node_modules/esbuild-windows-64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.25.tgz",
- "integrity": "sha512-TirEohRkfWU9hXLgoDxzhMQD1g8I2mOqvdQF2RS9E/wbkORTAqJHyh7wqGRCQAwNzdNXdg3JAyhQ9/177AadWA==",
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz",
+ "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "win32"
+ "freebsd"
],
"engines": {
- "node": ">=12"
+ "node": ">= 20"
}
},
- "node_modules/esbuild-windows-arm64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.25.tgz",
- "integrity": "sha512-4ype9ERiI45rSh+R8qUoBtaj6kJvUOI7oVLhKqPEpcF4Pa5PpT3hm/mXAyotJHREkHpM87PAJcA442mLnbtlNA==",
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz",
+ "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==",
"cpu": [
- "arm64"
+ "arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
- "win32"
+ "linux"
],
"engines": {
- "node": ">=12"
+ "node": ">= 20"
}
},
- "node_modules/estree-walker": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
- "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
- },
- "node_modules/follow-redirects": {
- "version": "1.14.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
- "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==",
- "funding": [
- {
- "type": "individual",
- "url": "https://github.com/sponsors/RubenVerborgh"
- }
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz",
+ "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
],
"engines": {
- "node": ">=4.0"
- },
- "peerDependenciesMeta": {
- "debug": {
- "optional": true
- }
+ "node": ">= 20"
}
},
- "node_modules/fsevents": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
- "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz",
+ "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
- "hasInstallScript": true,
+ "license": "MIT",
"optional": true,
"os": [
- "darwin"
+ "linux"
],
"engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ "node": ">= 20"
}
},
- "node_modules/function-bind": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
- "dev": true
- },
- "node_modules/has": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
- "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz",
+ "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "dependencies": {
- "function-bind": "^1.1.1"
- },
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
"engines": {
- "node": ">= 0.4.0"
+ "node": ">= 20"
}
},
- "node_modules/is-core-module": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
- "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz",
+ "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz",
+ "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
"dev": true,
+ "license": "MIT",
+ "optional": true,
"dependencies": {
- "has": "^1.0.3"
+ "@emnapi/core": "^1.8.1",
+ "@emnapi/runtime": "^1.8.1",
+ "@emnapi/wasi-threads": "^1.1.0",
+ "@napi-rs/wasm-runtime": "^1.1.1",
+ "@tybys/wasm-util": "^0.10.1",
+ "tslib": "^2.8.1"
},
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/magic-string": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
- "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
- "dependencies": {
- "sourcemap-codec": "^1.4.8"
+ "engines": {
+ "node": ">=14.0.0"
}
},
- "node_modules/nanoid": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
- "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz",
+ "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
"engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ "node": ">= 20"
}
},
- "node_modules/path-parse": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz",
+ "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz",
+ "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.2.2",
+ "@tailwindcss/oxide": "4.2.2",
+ "tailwindcss": "4.2.2"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz",
+ "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-rc.2"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@volar/language-core": {
+ "version": "2.4.28",
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz",
+ "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/source-map": "2.4.28"
+ }
+ },
+ "node_modules/@volar/source-map": {
+ "version": "2.4.28",
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz",
+ "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@volar/typescript": {
+ "version": "2.4.28",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz",
+ "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.28",
+ "path-browserify": "^1.0.1",
+ "vscode-uri": "^3.0.8"
+ }
+ },
+ "node_modules/@vue-macros/common": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz",
+ "integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-sfc": "^3.5.22",
+ "ast-kit": "^2.1.2",
+ "local-pkg": "^1.1.2",
+ "magic-string-ast": "^1.0.2",
+ "unplugin-utils": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/vue-macros"
+ },
+ "peerDependencies": {
+ "vue": "^2.7.0 || ^3.2.25"
+ },
+ "peerDependenciesMeta": {
+ "vue": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz",
+ "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@vue/shared": "3.5.30",
+ "entities": "^7.0.1",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz",
+ "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.30",
+ "@vue/shared": "3.5.30"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz",
+ "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@vue/compiler-core": "3.5.30",
+ "@vue/compiler-dom": "3.5.30",
+ "@vue/compiler-ssr": "3.5.30",
+ "@vue/shared": "3.5.30",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.21",
+ "postcss": "^8.5.8",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz",
+ "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.30",
+ "@vue/shared": "3.5.30"
+ }
+ },
+ "node_modules/@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==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/devtools-kit": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.1.tgz",
+ "integrity": "sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-shared": "^8.1.1",
+ "birpc": "^2.6.1",
+ "hookable": "^5.5.3",
+ "perfect-debounce": "^2.0.0"
+ }
+ },
+ "node_modules/@vue/devtools-shared": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.1.tgz",
+ "integrity": "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/language-core": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.2.6.tgz",
+ "integrity": "sha512-xYYYX3/aVup576tP/23sEUpgiEnujrENaoNRbaozC1/MA9I6EGFQRJb4xrt/MmUCAGlxTKL2RmT8JLTPqagCkg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@volar/language-core": "2.4.28",
+ "@vue/compiler-dom": "^3.5.0",
+ "@vue/shared": "^3.5.0",
+ "alien-signals": "^3.0.0",
+ "muggle-string": "^0.4.1",
+ "path-browserify": "^1.0.1",
+ "picomatch": "^4.0.2"
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz",
+ "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.30"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz",
+ "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.30",
+ "@vue/shared": "3.5.30"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz",
+ "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.30",
+ "@vue/runtime-core": "3.5.30",
+ "@vue/shared": "3.5.30",
+ "csstype": "^3.2.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz",
+ "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.30",
+ "@vue/shared": "3.5.30"
+ },
+ "peerDependencies": {
+ "vue": "3.5.30"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz",
+ "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==",
+ "license": "MIT"
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/alien-signals": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz",
+ "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ast-kit": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz",
+ "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "pathe": "^2.0.3"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ },
+ "node_modules/ast-walker-scope": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.8.3.tgz",
+ "integrity": "sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.4",
+ "ast-kit": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.15.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
+ "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/birpc": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
+ "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+ "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/confbox": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz",
+ "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==",
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.20.1",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
+ "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/exsolve": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
+ "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==",
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hookable": {
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+ "license": "MIT"
+ },
+ "node_modules/jiti": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/local-pkg": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz",
+ "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==",
+ "license": "MIT",
+ "dependencies": {
+ "mlly": "^1.7.4",
+ "pkg-types": "^2.3.0",
+ "quansync": "^0.2.11"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/lucide-vue-next": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-1.0.0.tgz",
+ "integrity": "sha512-V6SPvx1IHTj/UY+FrIYWV5faISsPSb8BnWSFDxAtezWKvWc9ZZ40PDrdu1/Qb5vg4lHWr1hs1BAMGVGm6V1Xdg==",
+ "dev": true,
+ "license": "ISC",
+ "peerDependencies": {
+ "vue": ">=3.0.1"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/magic-string-ast": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-1.0.3.tgz",
+ "integrity": "sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==",
+ "license": "MIT",
+ "dependencies": {
+ "magic-string": "^0.30.19"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mlly": {
+ "version": "1.8.2",
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
+ "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "pathe": "^2.0.3",
+ "pkg-types": "^1.3.1",
+ "ufo": "^1.6.3"
+ }
+ },
+ "node_modules/mlly/node_modules/confbox": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
+ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
+ "license": "MIT"
+ },
+ "node_modules/mlly/node_modules/pkg-types": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
+ "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "confbox": "^0.1.8",
+ "mlly": "^1.7.4",
+ "pathe": "^2.0.1"
+ }
},
- "node_modules/picocolors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+ "node_modules/muggle-string": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
+ "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
+ "license": "MIT"
},
- "node_modules/postcss": {
- "version": "8.4.8",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.8.tgz",
- "integrity": "sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ==",
- "dependencies": {
- "nanoid": "^3.3.1",
- "picocolors": "^1.0.0",
- "source-map-js": "^1.0.2"
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
},
"engines": {
- "node": "^10 || ^12 || >=14"
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "license": "MIT"
+ },
+ "node_modules/perfect-debounce": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz",
+ "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
},
"funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
+ "url": "https://github.com/sponsors/jonschlinkert"
}
},
- "node_modules/resolve": {
- "version": "1.22.0",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
- "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
- "dev": true,
+ "node_modules/pkg-types": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
+ "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
+ "license": "MIT",
+ "dependencies": {
+ "confbox": "^0.2.2",
+ "exsolve": "^1.0.7",
+ "pathe": "^2.0.3"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.14",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
+ "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+ "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"
+ }
+ ],
+ "license": "MIT",
"dependencies": {
- "is-core-module": "^2.8.1",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
},
- "bin": {
- "resolve": "bin/resolve"
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/quansync": {
+ "version": "0.2.11",
+ "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
+ "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/antfu"
+ },
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/sxzz"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/readdirp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
+ "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
}
},
- "node_modules/rollup": {
- "version": "2.63.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz",
- "integrity": "sha512-nps0idjmD+NXl6OREfyYXMn/dar3WGcyKn+KBzPdaLecub3x/LrId0wUcthcr8oZUAcZAR8NKcfGGFlNgGL1kQ==",
+ "node_modules/rolldown": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz",
+ "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==",
"dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.124.0",
+ "@rolldown/pluginutils": "1.0.0-rc.15"
+ },
"bin": {
- "rollup": "dist/bin/rollup"
+ "rolldown": "bin/cli.mjs"
},
"engines": {
- "node": ">=10.0.0"
+ "node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
- "fsevents": "~2.3.2"
- }
+ "@rolldown/binding-android-arm64": "1.0.0-rc.15",
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.15",
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.15",
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.15",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15"
+ }
+ },
+ "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.15",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz",
+ "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==",
+ "dev": true,
+ "license": "MIT"
},
- "node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "engines": {
- "node": ">=0.10.0"
- }
+ "node_modules/scule": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz",
+ "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
+ "license": "MIT"
},
"node_modules/source-map-js": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
- "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "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==",
+ "license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
- "node_modules/sourcemap-codec": {
- "version": "1.4.8",
- "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
- "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
+ "node_modules/tailwindcss": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
+ "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
+ "dev": true,
+ "license": "MIT"
},
- "node_modules/supports-preserve-symlinks-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "node_modules/tapable": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz",
+ "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==",
"dev": true,
+ "license": "MIT",
"engines": {
- "node": ">= 0.4"
+ "node": ">=6"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
}
},
- "node_modules/vant": {
- "version": "3.4.5",
- "resolved": "https://registry.npmjs.org/vant/-/vant-3.4.5.tgz",
- "integrity": "sha512-3an2v6ejW9pijKmaDbSCW7Hs1R45tqtTIcedfI8hEAA5zC2Bp8cXU6Z4gUCK6/RAGOJsB+vcyLINLb1cGS/SzQ==",
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "license": "MIT",
"dependencies": {
- "@vant/icons": "^1.7.1",
- "@vant/popperjs": "^1.1.0",
- "@vant/use": "^1.3.5"
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
},
- "peerDependencies": {
- "vue": "^3.0.0"
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "devOptional": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/ufo": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
+ "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
+ "license": "MIT"
+ },
+ "node_modules/unplugin": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz",
+ "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "picomatch": "^4.0.3",
+ "webpack-virtual-modules": "^0.6.2"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/unplugin-utils": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
+ "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
+ "license": "MIT",
+ "dependencies": {
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sxzz"
}
},
"node_modules/vite": {
- "version": "2.8.6",
- "resolved": "https://registry.npmjs.org/vite/-/vite-2.8.6.tgz",
- "integrity": "sha512-e4H0QpludOVKkmOsRyqQ7LTcMUDF3mcgyNU4lmi0B5JUbe0ZxeBBl8VoZ8Y6Rfn9eFKYtdXNPcYK97ZwH+K2ug==",
+ "version": "8.0.8",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz",
+ "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "esbuild": "^0.14.14",
- "postcss": "^8.4.6",
- "resolve": "^1.22.0",
- "rollup": "^2.59.0"
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.8",
+ "rolldown": "1.0.0-rc.15",
+ "tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
- "node": ">=12.2.0"
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
- "fsevents": "~2.3.2"
+ "fsevents": "~2.3.3"
},
"peerDependencies": {
- "less": "*",
- "sass": "*",
- "stylus": "*"
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
},
"peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
"less": {
"optional": true
},
"sass": {
"optional": true
},
+ "sass-embedded": {
+ "optional": true
+ },
"stylus": {
"optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
}
}
},
+ "node_modules/vscode-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/vue": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.31.tgz",
- "integrity": "sha512-odT3W2tcffTiQCy57nOT93INw1auq5lYLLYtWpPYQQYQOOdHiqFct9Xhna6GJ+pJQaF67yZABraH47oywkJgFw==",
+ "version": "3.5.30",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz",
+ "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==",
+ "license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.2.31",
- "@vue/compiler-sfc": "3.2.31",
- "@vue/runtime-dom": "3.2.31",
- "@vue/server-renderer": "3.2.31",
- "@vue/shared": "3.2.31"
+ "@vue/compiler-dom": "3.5.30",
+ "@vue/compiler-sfc": "3.5.30",
+ "@vue/runtime-dom": "3.5.30",
+ "@vue/server-renderer": "3.5.30",
+ "@vue/shared": "3.5.30"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
}
},
"node_modules/vue-i18n": {
- "version": "9.1.9",
- "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.1.9.tgz",
- "integrity": "sha512-JeRdNVxS2OGp1E+pye5XB6+M6BBkHwAv9C80Q7+kzoMdUDGRna06tjC0vCB/jDX9aWrl5swxOMFcyAr7or8XTA==",
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.0.tgz",
+ "integrity": "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA==",
+ "license": "MIT",
"dependencies": {
- "@intlify/core-base": "9.1.9",
- "@intlify/shared": "9.1.9",
- "@intlify/vue-devtools": "9.1.9",
- "@vue/devtools-api": "^6.0.0-beta.7"
+ "@intlify/core-base": "11.3.0",
+ "@intlify/devtools-types": "11.3.0",
+ "@intlify/shared": "11.3.0",
+ "@vue/devtools-api": "^6.5.0"
},
"engines": {
- "node": ">= 10"
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-router": {
- "version": "4.0.14",
- "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.14.tgz",
- "integrity": "sha512-wAO6zF9zxA3u+7AkMPqw9LjoUCjSxfFvINQj3E/DceTt6uEz1XZLraDhdg2EYmvVwTBSGlLYsUw8bDmx0754Mw==",
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.4.tgz",
+ "integrity": "sha512-lCqDLCI2+fKVRl2OzXuzdSWmxXFLQRxQbmHugnRpTMyYiT+hNaycV0faqG5FBHDXoYrZ6MQcX87BvbY8mQ20Bg==",
+ "license": "MIT",
"dependencies": {
- "@vue/devtools-api": "^6.0.0"
+ "@babel/generator": "^7.28.6",
+ "@vue-macros/common": "^3.1.1",
+ "@vue/devtools-api": "^8.0.6",
+ "ast-walker-scope": "^0.8.3",
+ "chokidar": "^5.0.0",
+ "json5": "^2.2.3",
+ "local-pkg": "^1.1.2",
+ "magic-string": "^0.30.21",
+ "mlly": "^1.8.0",
+ "muggle-string": "^0.4.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "scule": "^1.3.0",
+ "tinyglobby": "^0.2.15",
+ "unplugin": "^3.0.0",
+ "unplugin-utils": "^0.3.1",
+ "yaml": "^2.8.2"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
- "vue": "^3.2.0"
- }
- },
- "node_modules/yarn": {
- "version": "1.22.17",
- "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.17.tgz",
- "integrity": "sha512-H0p241BXaH0UN9IeH//RT82tl5PfNraVpSpEoW+ET7lmopNC61eZ+A+IDvU8FM6Go5vx162SncDL8J1ZjRBriQ==",
- "hasInstallScript": true,
- "bin": {
- "yarn": "bin/yarn.js",
- "yarnpkg": "bin/yarn.js"
+ "@pinia/colada": ">=0.21.2",
+ "@vue/compiler-sfc": "^3.5.17",
+ "pinia": "^3.0.4",
+ "vue": "^3.5.0"
},
- "engines": {
- "node": ">=4.0.0"
+ "peerDependenciesMeta": {
+ "@pinia/colada": {
+ "optional": true
+ },
+ "@vue/compiler-sfc": {
+ "optional": true
+ },
+ "pinia": {
+ "optional": true
+ }
}
- }
- },
- "dependencies": {
- "@babel/parser": {
- "version": "7.16.7",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.7.tgz",
- "integrity": "sha512-sR4eaSrnM7BV7QPzGfEX5paG/6wrZM3I0HDzfIAK06ESvo9oy3xBuVBxE3MbQaKNhvg8g/ixjMWo2CGpzpHsDA=="
- },
- "@icon-park/vue-next": {
- "version": "1.3.6",
- "resolved": "https://registry.npmjs.org/@icon-park/vue-next/-/vue-next-1.3.6.tgz",
- "integrity": "sha512-AZaCcjRPU9vTNVfcrG03CYJM+uv0SXx5lC4njanlRorFo+TV/x0ZTYGYGpTR/l6ek6QmFu9THmrgqKl7i/8yHg==",
- "requires": {}
- },
- "@intlify/core-base": {
- "version": "9.1.9",
- "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.1.9.tgz",
- "integrity": "sha512-x5T0p/Ja0S8hs5xs+ImKyYckVkL4CzcEXykVYYV6rcbXxJTe2o58IquSqX9bdncVKbRZP7GlBU1EcRaQEEJ+vw==",
- "requires": {
- "@intlify/devtools-if": "9.1.9",
- "@intlify/message-compiler": "9.1.9",
- "@intlify/message-resolver": "9.1.9",
- "@intlify/runtime": "9.1.9",
- "@intlify/shared": "9.1.9",
- "@intlify/vue-devtools": "9.1.9"
- }
- },
- "@intlify/devtools-if": {
- "version": "9.1.9",
- "resolved": "https://registry.npmjs.org/@intlify/devtools-if/-/devtools-if-9.1.9.tgz",
- "integrity": "sha512-oKSMKjttG3Ut/1UGEZjSdghuP3fwA15zpDPcjkf/1FjlOIm6uIBGMNS5jXzsZy593u+P/YcnrZD6cD3IVFz9vQ==",
- "requires": {
- "@intlify/shared": "9.1.9"
- }
- },
- "@intlify/message-compiler": {
- "version": "9.1.9",
- "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.1.9.tgz",
- "integrity": "sha512-6YgCMF46Xd0IH2hMRLCssZI3gFG4aywidoWQ3QP4RGYQXQYYfFC54DxhSgfIPpVoPLQ+4AD29eoYmhiHZ+qLFQ==",
- "requires": {
- "@intlify/message-resolver": "9.1.9",
- "@intlify/shared": "9.1.9",
- "source-map": "0.6.1"
- }
- },
- "@intlify/message-resolver": {
- "version": "9.1.9",
- "resolved": "https://registry.npmjs.org/@intlify/message-resolver/-/message-resolver-9.1.9.tgz",
- "integrity": "sha512-Lx/DBpigeK0sz2BBbzv5mu9/dAlt98HxwbG7xLawC3O2xMF9MNWU5FtOziwYG6TDIjNq0O/3ZbOJAxwITIWXEA=="
- },
- "@intlify/runtime": {
- "version": "9.1.9",
- "resolved": "https://registry.npmjs.org/@intlify/runtime/-/runtime-9.1.9.tgz",
- "integrity": "sha512-XgPw8+UlHCiie3fI41HPVa/VDJb3/aSH7bLhY1hJvlvNV713PFtb4p4Jo+rlE0gAoMsMCGcsiT982fImolSltg==",
- "requires": {
- "@intlify/message-compiler": "9.1.9",
- "@intlify/message-resolver": "9.1.9",
- "@intlify/shared": "9.1.9"
- }
- },
- "@intlify/shared": {
- "version": "9.1.9",
- "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.1.9.tgz",
- "integrity": "sha512-xKGM1d0EAxdDFCWedcYXOm6V5Pfw/TMudd6/qCdEb4tv0hk9EKeg7lwQF1azE0dP2phvx0yXxrt7UQK+IZjNdw=="
- },
- "@intlify/vue-devtools": {
- "version": "9.1.9",
- "resolved": "https://registry.npmjs.org/@intlify/vue-devtools/-/vue-devtools-9.1.9.tgz",
- "integrity": "sha512-YPehH9uL4vZcGXky4Ev5qQIITnHKIvsD2GKGXgqf+05osMUI6WSEQHaN9USRa318Rs8RyyPCiDfmA0hRu3k7og==",
- "requires": {
- "@intlify/message-resolver": "9.1.9",
- "@intlify/runtime": "9.1.9",
- "@intlify/shared": "9.1.9"
- }
- },
- "@popperjs/core": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz",
- "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA=="
- },
- "@vant/icons": {
- "version": "1.7.3",
- "resolved": "https://registry.npmjs.org/@vant/icons/-/icons-1.7.3.tgz",
- "integrity": "sha512-tW4EqzxN4kXw1rnlnQJQHofEifPbt/gECOWiibomht8QLyvoGuE4iUmDFS288dJ07ZjuTy0bhdABj0SENo2fmQ=="
- },
- "@vant/popperjs": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@vant/popperjs/-/popperjs-1.1.0.tgz",
- "integrity": "sha512-8MD1gz146awV/uPxYjz4pet22f7a9YVKqk7T+gFkWFwT9mEcrIUEg/xPrdOnWKLP9puXyYtm7oVfSDSefZ/p/w==",
- "requires": {
- "@popperjs/core": "^2.9.2"
- }
- },
- "@vant/use": {
- "version": "1.3.6",
- "resolved": "https://registry.npmjs.org/@vant/use/-/use-1.3.6.tgz",
- "integrity": "sha512-3z+nywPaV2F5BdJO7RQxWlgfzJeEOmViD2yHMb7Tg+R4NR/7iQskqW8v2Cnv9FWSJgTOSHlcr7UzeLpiTAP4HA=="
- },
- "@vitejs/plugin-vue": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-2.2.4.tgz",
- "integrity": "sha512-ev9AOlp0ljCaDkFZF3JwC/pD2N4Hh+r5srl5JHM6BKg5+99jiiK0rE/XaRs3pVm1wzyKkjUy/StBSoXX5fFzcw==",
- "dev": true,
- "requires": {}
- },
- "@vue/compiler-core": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.31.tgz",
- "integrity": "sha512-aKno00qoA4o+V/kR6i/pE+aP+esng5siNAVQ422TkBNM6qA4veXiZbSe8OTXHXquEi/f6Akc+nLfB4JGfe4/WQ==",
- "requires": {
- "@babel/parser": "^7.16.4",
- "@vue/shared": "3.2.31",
- "estree-walker": "^2.0.2",
- "source-map": "^0.6.1"
- }
- },
- "@vue/compiler-dom": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.31.tgz",
- "integrity": "sha512-60zIlFfzIDf3u91cqfqy9KhCKIJgPeqxgveH2L+87RcGU/alT6BRrk5JtUso0OibH3O7NXuNOQ0cDc9beT0wrg==",
- "requires": {
- "@vue/compiler-core": "3.2.31",
- "@vue/shared": "3.2.31"
- }
- },
- "@vue/compiler-sfc": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.31.tgz",
- "integrity": "sha512-748adc9msSPGzXgibHiO6T7RWgfnDcVQD+VVwYgSsyyY8Ans64tALHZANrKtOzvkwznV/F4H7OAod/jIlp/dkQ==",
- "requires": {
- "@babel/parser": "^7.16.4",
- "@vue/compiler-core": "3.2.31",
- "@vue/compiler-dom": "3.2.31",
- "@vue/compiler-ssr": "3.2.31",
- "@vue/reactivity-transform": "3.2.31",
- "@vue/shared": "3.2.31",
- "estree-walker": "^2.0.2",
- "magic-string": "^0.25.7",
- "postcss": "^8.1.10",
- "source-map": "^0.6.1"
- }
- },
- "@vue/compiler-ssr": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.31.tgz",
- "integrity": "sha512-mjN0rqig+A8TVDnsGPYJM5dpbjlXeHUm2oZHZwGyMYiGT/F4fhJf/cXy8QpjnLQK4Y9Et4GWzHn9PS8AHUnSkw==",
- "requires": {
- "@vue/compiler-dom": "3.2.31",
- "@vue/shared": "3.2.31"
- }
- },
- "@vue/devtools-api": {
- "version": "6.0.13",
- "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.13.tgz",
- "integrity": "sha512-T34EjcArVqzANedEZe2kXQ+AZsld2z1ptJlkOGm87+blk+s6udnP4ze/NYqV8lz1o9AIivimN0xxteLlWiWQdg=="
- },
- "@vue/reactivity": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.31.tgz",
- "integrity": "sha512-HVr0l211gbhpEKYr2hYe7hRsV91uIVGFYNHj73njbARVGHQvIojkImKMaZNDdoDZOIkMsBc9a1sMqR+WZwfSCw==",
- "requires": {
- "@vue/shared": "3.2.31"
- }
- },
- "@vue/reactivity-transform": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.31.tgz",
- "integrity": "sha512-uS4l4z/W7wXdI+Va5pgVxBJ345wyGFKvpPYtdSgvfJfX/x2Ymm6ophQlXXB6acqGHtXuBqNyyO3zVp9b1r0MOA==",
- "requires": {
- "@babel/parser": "^7.16.4",
- "@vue/compiler-core": "3.2.31",
- "@vue/shared": "3.2.31",
- "estree-walker": "^2.0.2",
- "magic-string": "^0.25.7"
- }
- },
- "@vue/runtime-core": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.31.tgz",
- "integrity": "sha512-Kcog5XmSY7VHFEMuk4+Gap8gUssYMZ2+w+cmGI6OpZWYOEIcbE0TPzzPHi+8XTzAgx1w/ZxDFcXhZeXN5eKWsA==",
- "requires": {
- "@vue/reactivity": "3.2.31",
- "@vue/shared": "3.2.31"
- }
- },
- "@vue/runtime-dom": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.31.tgz",
- "integrity": "sha512-N+o0sICVLScUjfLG7u9u5XCjvmsexAiPt17GNnaWHJUfsKed5e85/A3SWgKxzlxx2SW/Hw7RQxzxbXez9PtY3g==",
- "requires": {
- "@vue/runtime-core": "3.2.31",
- "@vue/shared": "3.2.31",
- "csstype": "^2.6.8"
- }
- },
- "@vue/server-renderer": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.31.tgz",
- "integrity": "sha512-8CN3Zj2HyR2LQQBHZ61HexF5NReqngLT3oahyiVRfSSvak+oAvVmu8iNLSu6XR77Ili2AOpnAt1y8ywjjqtmkg==",
- "requires": {
- "@vue/compiler-ssr": "3.2.31",
- "@vue/shared": "3.2.31"
- }
- },
- "@vue/shared": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.31.tgz",
- "integrity": "sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ=="
- },
- "axios": {
- "version": "0.26.1",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
- "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
- "requires": {
- "follow-redirects": "^1.14.8"
- }
- },
- "csstype": {
- "version": "2.6.20",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
- "integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
- },
- "esbuild": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.25.tgz",
- "integrity": "sha512-4JHEIOMNFvK09ziiL+iVmldIhLbn49V4NAVo888tcGFKedEZY/Y8YapfStJ6zSE23tzYPKxqKwQBnQoIO0BI/Q==",
- "dev": true,
- "requires": {
- "esbuild-android-64": "0.14.25",
- "esbuild-android-arm64": "0.14.25",
- "esbuild-darwin-64": "0.14.25",
- "esbuild-darwin-arm64": "0.14.25",
- "esbuild-freebsd-64": "0.14.25",
- "esbuild-freebsd-arm64": "0.14.25",
- "esbuild-linux-32": "0.14.25",
- "esbuild-linux-64": "0.14.25",
- "esbuild-linux-arm": "0.14.25",
- "esbuild-linux-arm64": "0.14.25",
- "esbuild-linux-mips64le": "0.14.25",
- "esbuild-linux-ppc64le": "0.14.25",
- "esbuild-linux-riscv64": "0.14.25",
- "esbuild-linux-s390x": "0.14.25",
- "esbuild-netbsd-64": "0.14.25",
- "esbuild-openbsd-64": "0.14.25",
- "esbuild-sunos-64": "0.14.25",
- "esbuild-windows-32": "0.14.25",
- "esbuild-windows-64": "0.14.25",
- "esbuild-windows-arm64": "0.14.25"
- }
- },
- "esbuild-android-64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.25.tgz",
- "integrity": "sha512-L5vCUk7TzFbBnoESNoXjU3x9+/+7TDIE/1mTfy/erAfvZAqC+S3sp/Qa9wkypFMcFvN9FzvESkTlpeQDolREtQ==",
- "dev": true,
- "optional": true
- },
- "esbuild-android-arm64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.25.tgz",
- "integrity": "sha512-4jv5xPjM/qNm27T5j3ZEck0PvjgQtoMHnz4FzwF5zNP56PvY2CT0WStcAIl6jNlsuDdN63rk2HRBIsO6xFbcFw==",
- "dev": true,
- "optional": true
- },
- "esbuild-darwin-64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.25.tgz",
- "integrity": "sha512-TGp8tuudIxOyWd1+8aYPxQmC1ZQyvij/AfNBa35RubixD0zJ1vkKHVAzo0Zao1zcG6pNqiSyzfPto8vmg0s7oA==",
- "dev": true,
- "optional": true
- },
- "esbuild-darwin-arm64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.25.tgz",
- "integrity": "sha512-oTcDgdm0MDVEmw2DWu8BV68pYuImpFgvWREPErBZmNA4MYKGuBRaCiJqq6jZmBR1x+3y1DWCjez+5uLtuAm6mw==",
- "dev": true,
- "optional": true
- },
- "esbuild-freebsd-64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.25.tgz",
- "integrity": "sha512-ueAqbnMZ8arnuLH8tHwTCQYeptnHOUV7vA6px6j4zjjQwDx7TdP7kACPf3TLZLdJQ3CAD1XCvQ2sPhX+8tacvQ==",
- "dev": true,
- "optional": true
- },
- "esbuild-freebsd-arm64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.25.tgz",
- "integrity": "sha512-+ZVWud2HKh+Ob6k/qiJWjBtUg4KmJGGmbvEXXW1SNKS7hW7HU+Zq2ZCcE1akFxOPkVB+EhOty/sSek30tkCYug==",
- "dev": true,
- "optional": true
- },
- "esbuild-linux-32": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.25.tgz",
- "integrity": "sha512-3OP/lwV3kCzEz45tobH9nj+uE4ubhGsfx+tn0L26WAGtUbmmcRpqy7XRG/qK7h1mClZ+eguIANcQntYMdYklfw==",
- "dev": true,
- "optional": true
- },
- "esbuild-linux-64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.25.tgz",
- "integrity": "sha512-+aKHdHZmX9qwVlQmu5xYXh7GsBFf4TWrePgeJTalhXHOG7NNuUwoHmketGiZEoNsWyyqwH9rE5BC+iwcLY30Ug==",
- "dev": true,
- "optional": true
- },
- "esbuild-linux-arm": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.25.tgz",
- "integrity": "sha512-aTLcE2VBoLydL943REcAcgnDi3bHtmULSXWLbjtBdtykRatJVSxKMjK9YlBXUZC4/YcNQfH7AxwVeQr9fNxPhw==",
- "dev": true,
- "optional": true
- },
- "esbuild-linux-arm64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.25.tgz",
- "integrity": "sha512-UxfenPx/wSZx55gScCImPtXekvZQLI2GW3qe5dtlmU7luiqhp5GWPzGeQEbD3yN3xg/pHc671m5bma5Ns7lBHw==",
- "dev": true,
- "optional": true
- },
- "esbuild-linux-mips64le": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.25.tgz",
- "integrity": "sha512-wLWYyqVfYx9Ur6eU5RT92yJVsaBGi5RdkoWqRHOqcJ38Kn60QMlcghsKeWfe9jcYut8LangYZ98xO1LxIoSXrQ==",
- "dev": true,
- "optional": true
- },
- "esbuild-linux-ppc64le": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.25.tgz",
- "integrity": "sha512-0dR6Csl6Zas3g4p9ULckEl8Mo8IInJh33VCJ3eaV1hj9+MHGdmDOakYMN8MZP9/5nl+NU/0ygpd14cWgy8uqRw==",
- "dev": true,
- "optional": true
- },
- "esbuild-linux-riscv64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.25.tgz",
- "integrity": "sha512-J4d20HDmTrgvhR0bdkDhvvJGaikH3LzXQnNaseo8rcw9Yqby9A90gKUmWpfwqLVNRILvNnAmKLfBjCKU9ajg8w==",
- "dev": true,
- "optional": true
- },
- "esbuild-linux-s390x": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.25.tgz",
- "integrity": "sha512-YI2d5V6nTE73ZnhEKQD7MtsPs1EtUZJ3obS21oxQxGbbRw1G+PtJKjNyur+3t6nzHP9oTg6GHQ3S3hOLLmbDIQ==",
- "dev": true,
- "optional": true
- },
- "esbuild-netbsd-64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.25.tgz",
- "integrity": "sha512-TKIVgNWLUOkr+Exrye70XTEE1lJjdQXdM4tAXRzfHE9iBA7LXWcNtVIuSnphTqpanPzTDFarF0yqq4kpbC6miA==",
- "dev": true,
- "optional": true
- },
- "esbuild-openbsd-64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.25.tgz",
- "integrity": "sha512-QgFJ37A15D7NIXBTYEqz29+uw3nNBOIyog+3kFidANn6kjw0GHZ0lEYQn+cwjyzu94WobR+fes7cTl/ZYlHb1A==",
- "dev": true,
- "optional": true
- },
- "esbuild-sunos-64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.25.tgz",
- "integrity": "sha512-rmWfjUItYIVlqr5EnTH1+GCxXiBOC42WBZ3w++qh7n2cS9Xo0lO5pGSG2N+huOU2fX5L+6YUuJ78/vOYvefeFw==",
- "dev": true,
- "optional": true
- },
- "esbuild-windows-32": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.25.tgz",
- "integrity": "sha512-HGAxVUofl3iUIz9W10Y9XKtD0bNsK9fBXv1D55N/ljNvkrAYcGB8YCm0v7DjlwtyS6ws3dkdQyXadbxkbzaKOA==",
- "dev": true,
- "optional": true
- },
- "esbuild-windows-64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.25.tgz",
- "integrity": "sha512-TirEohRkfWU9hXLgoDxzhMQD1g8I2mOqvdQF2RS9E/wbkORTAqJHyh7wqGRCQAwNzdNXdg3JAyhQ9/177AadWA==",
- "dev": true,
- "optional": true
- },
- "esbuild-windows-arm64": {
- "version": "0.14.25",
- "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.25.tgz",
- "integrity": "sha512-4ype9ERiI45rSh+R8qUoBtaj6kJvUOI7oVLhKqPEpcF4Pa5PpT3hm/mXAyotJHREkHpM87PAJcA442mLnbtlNA==",
- "dev": true,
- "optional": true
- },
- "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=="
- },
- "follow-redirects": {
- "version": "1.14.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz",
- "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w=="
- },
- "fsevents": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
- "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
- "dev": true,
- "optional": true
- },
- "function-bind": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
- "dev": true
},
- "has": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
- "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
- "dev": true,
- "requires": {
- "function-bind": "^1.1.1"
+ "node_modules/vue-router/node_modules/@vue/devtools-api": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.1.tgz",
+ "integrity": "sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-kit": "^8.1.1"
}
},
- "is-core-module": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
- "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
+ "node_modules/vue-tsc": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.2.6.tgz",
+ "integrity": "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q==",
"dev": true,
- "requires": {
- "has": "^1.0.3"
- }
- },
- "magic-string": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
- "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
- "requires": {
- "sourcemap-codec": "^1.4.8"
+ "license": "MIT",
+ "dependencies": {
+ "@volar/typescript": "2.4.28",
+ "@vue/language-core": "3.2.6"
+ },
+ "bin": {
+ "vue-tsc": "bin/vue-tsc.js"
+ },
+ "peerDependencies": {
+ "typescript": ">=5.0.0"
}
},
- "nanoid": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
- "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw=="
- },
- "path-parse": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true
- },
- "picocolors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
- },
- "postcss": {
- "version": "8.4.8",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.8.tgz",
- "integrity": "sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ==",
- "requires": {
- "nanoid": "^3.3.1",
- "picocolors": "^1.0.0",
- "source-map-js": "^1.0.2"
- }
+ "node_modules/webpack-virtual-modules": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
+ "license": "MIT"
},
- "resolve": {
- "version": "1.22.0",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
- "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==",
- "dev": true,
- "requires": {
- "is-core-module": "^2.8.1",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
+ "node_modules/yaml": {
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
+ "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/eemeli"
}
},
- "rollup": {
- "version": "2.63.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz",
- "integrity": "sha512-nps0idjmD+NXl6OREfyYXMn/dar3WGcyKn+KBzPdaLecub3x/LrId0wUcthcr8oZUAcZAR8NKcfGGFlNgGL1kQ==",
- "dev": true,
- "requires": {
- "fsevents": "~2.3.2"
+ "node_modules/yarn": {
+ "version": "1.22.22",
+ "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.22.tgz",
+ "integrity": "sha512-prL3kGtyG7o9Z9Sv8IPfBNrWTDmXB4Qbes8A9rEzt6wkJV8mUvoirjU0Mp3GGAU06Y0XQyA3/2/RQFVuK7MTfg==",
+ "hasInstallScript": true,
+ "license": "BSD-2-Clause",
+ "bin": {
+ "yarn": "bin/yarn.js",
+ "yarnpkg": "bin/yarn.js"
+ },
+ "engines": {
+ "node": ">=4.0.0"
}
- },
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
- },
- "source-map-js": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
- "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
- },
- "sourcemap-codec": {
- "version": "1.4.8",
- "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
- "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
- },
- "supports-preserve-symlinks-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "dev": true
- },
- "vant": {
- "version": "3.4.5",
- "resolved": "https://registry.npmjs.org/vant/-/vant-3.4.5.tgz",
- "integrity": "sha512-3an2v6ejW9pijKmaDbSCW7Hs1R45tqtTIcedfI8hEAA5zC2Bp8cXU6Z4gUCK6/RAGOJsB+vcyLINLb1cGS/SzQ==",
- "requires": {
- "@vant/icons": "^1.7.1",
- "@vant/popperjs": "^1.1.0",
- "@vant/use": "^1.3.5"
- }
- },
- "vite": {
- "version": "2.8.6",
- "resolved": "https://registry.npmjs.org/vite/-/vite-2.8.6.tgz",
- "integrity": "sha512-e4H0QpludOVKkmOsRyqQ7LTcMUDF3mcgyNU4lmi0B5JUbe0ZxeBBl8VoZ8Y6Rfn9eFKYtdXNPcYK97ZwH+K2ug==",
- "dev": true,
- "requires": {
- "esbuild": "^0.14.14",
- "fsevents": "~2.3.2",
- "postcss": "^8.4.6",
- "resolve": "^1.22.0",
- "rollup": "^2.59.0"
- }
- },
- "vue": {
- "version": "3.2.31",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.31.tgz",
- "integrity": "sha512-odT3W2tcffTiQCy57nOT93INw1auq5lYLLYtWpPYQQYQOOdHiqFct9Xhna6GJ+pJQaF67yZABraH47oywkJgFw==",
- "requires": {
- "@vue/compiler-dom": "3.2.31",
- "@vue/compiler-sfc": "3.2.31",
- "@vue/runtime-dom": "3.2.31",
- "@vue/server-renderer": "3.2.31",
- "@vue/shared": "3.2.31"
- }
- },
- "vue-i18n": {
- "version": "9.1.9",
- "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.1.9.tgz",
- "integrity": "sha512-JeRdNVxS2OGp1E+pye5XB6+M6BBkHwAv9C80Q7+kzoMdUDGRna06tjC0vCB/jDX9aWrl5swxOMFcyAr7or8XTA==",
- "requires": {
- "@intlify/core-base": "9.1.9",
- "@intlify/shared": "9.1.9",
- "@intlify/vue-devtools": "9.1.9",
- "@vue/devtools-api": "^6.0.0-beta.7"
- }
- },
- "vue-router": {
- "version": "4.0.14",
- "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.14.tgz",
- "integrity": "sha512-wAO6zF9zxA3u+7AkMPqw9LjoUCjSxfFvINQj3E/DceTt6uEz1XZLraDhdg2EYmvVwTBSGlLYsUw8bDmx0754Mw==",
- "requires": {
- "@vue/devtools-api": "^6.0.0"
- }
- },
- "yarn": {
- "version": "1.22.17",
- "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.17.tgz",
- "integrity": "sha512-H0p241BXaH0UN9IeH//RT82tl5PfNraVpSpEoW+ET7lmopNC61eZ+A+IDvU8FM6Go5vx162SncDL8J1ZjRBriQ=="
}
}
}
diff --git a/rootfs/web/package.json b/rootfs/web/package.json
index bd7d73b..82a5d5e 100644
--- a/rootfs/web/package.json
+++ b/rootfs/web/package.json
@@ -8,17 +8,20 @@
"serve": "vite preview"
},
"dependencies": {
- "@icon-park/vue-next": "^1.3.6",
- "axios": "^0.26.1",
- "vant": "^3.4.5",
- "vue": "^3.2.31",
- "vue-i18n": "^9.1.9",
- "vue-router": "^4.0.14",
- "yarn": "^1.22.17"
+ "axios": "^1.15.2",
+ "vue": "^3.5.30",
+ "vue-i18n": "^11.3.0",
+ "vue-router": "^5.0.4",
+ "yarn": "^1.22.22"
},
"devDependencies": {
- "@vitejs/plugin-vue": "^2.2.4",
- "@vue/compiler-sfc": "^3.2.31",
- "vite": "^2.8.6"
+ "@tailwindcss/vite": "^4.2.2",
+ "@vitejs/plugin-vue": "^6.0.5",
+ "@vue/compiler-sfc": "^3.5.30",
+ "lucide-vue-next": "^1.0.0",
+ "tailwindcss": "^4.2.2",
+ "typescript": "^6.0.3",
+ "vite": "^8.0.8",
+ "vue-tsc": "^3.2.6"
}
}
diff --git a/rootfs/web/src/.env b/rootfs/web/src/.env
deleted file mode 100644
index cba474f..0000000
--- a/rootfs/web/src/.env
+++ /dev/null
@@ -1 +0,0 @@
-VITE_APP_BASE_API=/
\ No newline at end of file
diff --git a/rootfs/web/src/assets/icons/drycc.svg b/rootfs/web/src/assets/icons/drycc.svg
new file mode 100644
index 0000000..5a7cdca
--- /dev/null
+++ b/rootfs/web/src/assets/icons/drycc.svg
@@ -0,0 +1,13 @@
+
diff --git a/rootfs/web/src/assets/icons/github.svg b/rootfs/web/src/assets/icons/github.svg
new file mode 100644
index 0000000..dc2f59a
--- /dev/null
+++ b/rootfs/web/src/assets/icons/github.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/rootfs/web/src/assets/icons/google.svg b/rootfs/web/src/assets/icons/google.svg
new file mode 100644
index 0000000..e9f9e65
--- /dev/null
+++ b/rootfs/web/src/assets/icons/google.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/rootfs/web/src/assets/images/avatar.png b/rootfs/web/src/assets/images/avatar.png
deleted file mode 100644
index a09db42..0000000
Binary files a/rootfs/web/src/assets/images/avatar.png and /dev/null differ
diff --git a/rootfs/web/src/components/AccessTokenDelete.js b/rootfs/web/src/components/AccessTokenDelete.js
deleted file mode 100644
index 0b1eb41..0000000
--- a/rootfs/web/src/components/AccessTokenDelete.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { reactive, toRefs } from 'vue'
-import { Toast } from "vant";
-import {deleteAccessToken} from "../services/tokens";
-
-export default {
- name: "AccessTokenDelete",
- props: {
- token: [Object, Function],
- },
- setup(props, context) {
- const state = reactive({
- token: props.token,
- })
-
- const canelDelete = () => {
- context.emit('closeDelete', { hasAccessTokenDeleted: false })
- }
-
- const deleteToken = () => {
- deleteAccessToken(state.token.id).then(res=>{
- if (res.status == 204) {
- Toast.success("OK")
- context.emit('closeDelete', { hasAccessTokenDeleted: true })
- }
- })
- }
- return {
- ...toRefs(state),
- canelDelete,
- deleteToken
- }
- }
-}
diff --git a/rootfs/web/src/components/AccessTokenDelete.vue b/rootfs/web/src/components/AccessTokenDelete.vue
index 5420465..f7473cf 100644
--- a/rootfs/web/src/components/AccessTokenDelete.vue
+++ b/rootfs/web/src/components/AccessTokenDelete.vue
@@ -1,31 +1,30 @@
-