From c1d0da683eebdc4d4dec9f6744849e22f63841de Mon Sep 17 00:00:00 2001 From: Ken Lippold Date: Thu, 8 Oct 2026 16:40:49 -0700 Subject: [PATCH 1/3] Remove built-in admin login page --- django/hydroserver/admin.py | 41 +++++++++- django/hydroserver/apps.py | 5 ++ django/hydroserver/settings.py | 3 +- django/templates/account/profile.html | 7 ++ django/tests/hydroserver/test_admin_auth.py | 87 +++++++++++++++++++++ 5 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 django/hydroserver/apps.py create mode 100644 django/tests/hydroserver/test_admin_auth.py diff --git a/django/hydroserver/admin.py b/django/hydroserver/admin.py index bab152a25..2c1c74174 100644 --- a/django/hydroserver/admin.py +++ b/django/hydroserver/admin.py @@ -1,11 +1,50 @@ from django.db import transaction from django.http import HttpResponseRedirect from django.core.management import call_command -from django.contrib import messages +from django.contrib import admin, messages +from django.contrib.auth import REDIRECT_FIELD_NAME +from django.contrib.auth.decorators import login_not_required +from django.contrib.auth.views import redirect_to_login +from django.shortcuts import redirect from django.urls import reverse +from django.utils.decorators import method_decorator +from django.utils.http import url_has_allowed_host_and_scheme +from django.views.decorators.cache import never_cache from django.conf import settings +class HydroServerAdminSite(admin.AdminSite): + """Admin site that delegates login, logout, and password changes to allauth.""" + + @method_decorator(never_cache) + @login_not_required + def login(self, request, extra_context=None): + next_url = request.GET.get(REDIRECT_FIELD_NAME, "") + if not url_has_allowed_host_and_scheme( + next_url, + allowed_hosts={request.get_host()}, + require_https=request.is_secure(), + ): + next_url = reverse("admin:index", current_app=self.name) + + if not request.user.is_authenticated: + return redirect_to_login(next_url) + + if not self.has_permission(request): + messages.error( + request, "You don't have permission to access the admin dashboard." + ) + return redirect("account_profile") + + return HttpResponseRedirect(next_url) + + def logout(self, request, extra_context=None): + return redirect("account_logout") + + def password_change(self, request, extra_context=None): + return redirect("account_change_password") + + class VocabularyAdmin: @transaction.atomic def load_fixtures(self, request, redirect, fixtures): diff --git a/django/hydroserver/apps.py b/django/hydroserver/apps.py new file mode 100644 index 000000000..8f6769dca --- /dev/null +++ b/django/hydroserver/apps.py @@ -0,0 +1,5 @@ +from django.contrib.admin.apps import AdminConfig + + +class HydroServerAdminConfig(AdminConfig): + default_site = "hydroserver.admin.HydroServerAdminSite" diff --git a/django/hydroserver/settings.py b/django/hydroserver/settings.py index 42d1428d4..15115bbf2 100644 --- a/django/hydroserver/settings.py +++ b/django/hydroserver/settings.py @@ -113,7 +113,7 @@ "processing.products.apps.ProductsConfig", "processing.monitoring.apps.MonitoringConfig", "processing.quality.apps.QualityConfig", - "django.contrib.admin", + "hydroserver.apps.HydroServerAdminConfig", ] MIDDLEWARE = [ @@ -259,6 +259,7 @@ ACCOUNT_LOGOUT_ON_GET = True ACCOUNT_DEFAULT_HTTP_PROTOCOL = urlparse(PROXY_BASE_URL).scheme +LOGIN_URL = "account_login" LOGIN_REDIRECT_URL = WEB_CLIENT_URL LOGOUT_REDIRECT_URL = WEB_CLIENT_URL diff --git a/django/templates/account/profile.html b/django/templates/account/profile.html index 869dcac51..aa6f873fd 100644 --- a/django/templates/account/profile.html +++ b/django/templates/account/profile.html @@ -8,6 +8,12 @@

{% trans "Profile" %}

{% trans "Edit" %} +{% if messages %} +
+ {% for message in messages %}

{{ message }}

{% endfor %} +
+{% endif %} +

{% trans "Personal information" %}

@@ -63,6 +69,7 @@

{% trans "Organization" %}

{% trans "Account management" %}

diff --git a/django/tests/hydroserver/test_admin_auth.py b/django/tests/hydroserver/test_admin_auth.py new file mode 100644 index 000000000..13a402ce0 --- /dev/null +++ b/django/tests/hydroserver/test_admin_auth.py @@ -0,0 +1,87 @@ +from urllib.parse import quote + +import pytest +from django.urls import reverse + +from tests.core.iam.factories import UserFactory + +pytestmark = pytest.mark.django_db + +ADMIN_PAGE = "/admin/iam/user/" + + +def test_anonymous_admin_access_redirects_to_account_login(client): + response = client.get(ADMIN_PAGE) + + assert response.status_code == 302 + login_redirect = response.url + response = client.get(login_redirect) + + assert response.status_code == 302 + assert response.url == f"{reverse('account_login')}?next={quote(ADMIN_PAGE)}" + + +def test_admin_login_page_never_renders_django_form(client): + response = client.get(reverse("admin:login")) + + assert response.status_code == 302 + assert response.url.startswith(reverse("account_login")) + + +def test_admin_login_rejects_external_next(client): + response = client.get(f"{reverse('admin:login')}?next=https://evil.example.com/") + + assert response.url == ( + f"{reverse('account_login')}?next={quote(reverse('admin:index'))}" + ) + + +def test_account_login_returns_to_admin_page(client): + user = UserFactory(is_staff=True, password="password") + + response = client.post( + reverse("account_login"), + {"login": user.email, "password": "password", "next": ADMIN_PAGE}, + ) + + assert response.status_code == 302 + assert response.url == ADMIN_PAGE + + +def test_non_staff_user_is_sent_to_profile_with_banner(client): + client.force_login(UserFactory()) + + response = client.get(ADMIN_PAGE, follow=True) + + assert response.redirect_chain[-1][0] == reverse("account_profile") + assert b"permission to access the admin dashboard" in response.content + + +def test_staff_user_can_access_admin(client): + client.force_login(UserFactory(is_staff=True, is_superuser=True)) + + assert client.get(ADMIN_PAGE).status_code == 200 + + +def test_staff_user_on_admin_login_is_sent_to_next(client): + client.force_login(UserFactory(is_staff=True)) + + response = client.get(f"{reverse('admin:login')}?next={ADMIN_PAGE}") + + assert response.url == ADMIN_PAGE + + +def test_admin_logout_uses_allauth(client): + client.force_login(UserFactory(is_staff=True)) + + response = client.post(reverse("admin:logout")) + + assert response.url == reverse("account_logout") + + +def test_admin_password_change_uses_allauth(client): + client.force_login(UserFactory(is_staff=True)) + + response = client.get(reverse("admin:password_change")) + + assert response.url == reverse("account_change_password") From ccfc0b7d8108b47d1b972fadb957a7fb1ffbd8a4 Mon Sep 17 00:00:00 2001 From: Ken Lippold Date: Fri, 9 Oct 2026 08:38:43 -0700 Subject: [PATCH 2/3] Removed old accountType property --- .../src/components/base/Navbar.vue | 17 ++++ .../components/base/__tests__/Navbar.spec.ts | 88 +++++++++++++++++++ .../composables/useWorkspacePermissions.ts | 2 +- apps/data-management/src/pages/Workspaces.vue | 4 +- .../src/pages/__tests__/Orchestration.spec.ts | 2 +- django/core/iam/models/user.py | 19 +--- django/tests/core/iam/models/test_user.py | 25 ++++++ django/tests/hydroserver/test_oidc_auth.py | 11 ++- .../src/api/__tests__/user.service.spec.ts | 3 +- .../src/api/services/user.service.ts | 4 +- packages/hydroserver-ts/src/types/index.ts | 9 +- packages/qc-utils/src/types/index.ts | 9 +- 12 files changed, 160 insertions(+), 33 deletions(-) create mode 100644 apps/data-management/src/components/base/__tests__/Navbar.spec.ts diff --git a/apps/data-management/src/components/base/Navbar.vue b/apps/data-management/src/components/base/Navbar.vue index 9359fb6c3..c2d380354 100644 --- a/apps/data-management/src/components/base/Navbar.vue +++ b/apps/data-management/src/components/base/Navbar.vue @@ -94,6 +94,14 @@ title="About" /> + + About + Admin dashboard ({ + useRoute: () => ({ meta: {}, name: 'Workspaces', path: '/workspaces' }), +})) + +vi.mock('@/router/router', () => ({ + default: { + push: vi.fn(), + resolve: () => ({ meta: {} }), + }, +})) + +vi.mock('@/store/dataVisualization', () => ({ + useDataVisStore: () => ({ resetState: vi.fn() }), +})) + +vi.mock('@hydroserver/client', () => ({ + default: { + resolveUrl: (path: string) => `https://hydro.example.com${path}`, + session: { + isAuthenticated: true, + getAccountSignupUrl: () => '', + }, + }, + User: class { + firstName = '' + lastName = '' + isStaff = false + }, +})) + +const ADMIN_URL = 'https://hydro.example.com/admin/' + +/** `compact` mirrors the narrow-viewport media query that swaps in the drawer. */ +async function mountNavbar(isStaff: boolean, compact = false) { + const user = new User() + user.isStaff = isStaff + useUserStore().user = user + + window.matchMedia = vi.fn().mockReturnValue({ + matches: compact, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + }) + + const wrapper = shallowMount(Navbar, { + global: { renderStubDefaultSlot: true }, + }) + await nextTick() + return wrapper +} + +describe('Navbar admin dashboard link', () => { + beforeEach(() => { + setActivePinia(createPinia()) + }) + + it('shows staff users the link in the account menu', async () => { + const wrapper = await mountNavbar(true) + + expect( + wrapper.get('[data-testid="admin-menu-item"]').attributes('href') + ).toBe(ADMIN_URL) + }) + + it('shows staff users the link in the compact drawer', async () => { + const wrapper = await mountNavbar(true, true) + + expect( + wrapper.get('[data-testid="admin-drawer-item"]').attributes('href') + ).toBe(ADMIN_URL) + }) + + it('hides the link from non-staff users', async () => { + for (const compact of [false, true]) { + const wrapper = await mountNavbar(false, compact) + + expect(wrapper.find('[data-testid^="admin-"]').exists()).toBe(false) + } + }) +}) diff --git a/apps/data-management/src/composables/useWorkspacePermissions.ts b/apps/data-management/src/composables/useWorkspacePermissions.ts index e6a4d9fcf..a71ab65ec 100644 --- a/apps/data-management/src/composables/useWorkspacePermissions.ts +++ b/apps/data-management/src/composables/useWorkspacePermissions.ts @@ -35,7 +35,7 @@ export function useWorkspacePermissions( } function isAdmin() { - return user.value?.accountType === 'admin' + return user.value?.isSuperuser === true } const getUserRoleName = (workspace: Workspace): string => { if (isOwner(workspace)) return 'Owner' diff --git a/apps/data-management/src/pages/Workspaces.vue b/apps/data-management/src/pages/Workspaces.vue index 26ce11a95..b6905f434 100644 --- a/apps/data-management/src/pages/Workspaces.vue +++ b/apps/data-management/src/pages/Workspaces.vue @@ -406,9 +406,7 @@ watch( { immediate: true } ) -const canCreateWorkspace = computed(() => - ['admin', 'standard'].includes(user.value?.accountType ?? '') -) +const canCreateWorkspace = computed(() => user.value?.ownedWorkspaceLimit !== 0) /** Workspaces that are pending a transfer to the current user */ const pendingWorkspaces = computed(() => diff --git a/apps/data-management/src/pages/__tests__/Orchestration.spec.ts b/apps/data-management/src/pages/__tests__/Orchestration.spec.ts index 11ea0c24c..7b8f74120 100644 --- a/apps/data-management/src/pages/__tests__/Orchestration.spec.ts +++ b/apps/data-management/src/pages/__tests__/Orchestration.spec.ts @@ -64,7 +64,7 @@ vi.mock('@hydroserver/client', () => ({ }, DataConnection: class {}, User: class { - accountType = 'standard' + isSuperuser = false email = 'user@example.com' }, PermissionAction: { diff --git a/django/core/iam/models/user.py b/django/core/iam/models/user.py index d3c59cd7e..b8001ef14 100644 --- a/django/core/iam/models/user.py +++ b/django/core/iam/models/user.py @@ -98,14 +98,6 @@ def name(self): def organization_name(self): return self.organization.name if self.organization else None - @property - def account_type(self): - if self.is_superuser: - return "admin" - if self.owned_workspace_limit == 0: - return "limited" - return "standard" - def save(self, *args, **kwargs): self.email = self.email.lower() super().save(*args, **kwargs) @@ -118,13 +110,6 @@ def to_profile_claims(self) -> dict: surfaces describe a user the same way. """ - if self.is_superuser: - account_type = "admin" - elif self.owned_workspace_limit == 0: - account_type = "limited" - else: - account_type = "standard" - organization = ( { "name": self.organization.name, @@ -147,7 +132,9 @@ def to_profile_claims(self) -> dict: "address": self.address, "link": self.link, "type": self.user_type, - "accountType": account_type, + "isStaff": self.is_staff, + "isSuperuser": self.is_superuser, + "ownedWorkspaceLimit": self.owned_workspace_limit, "organization": organization, "hydroShareConnected": self.socialaccount_set.filter( provider="hydroshare" diff --git a/django/tests/core/iam/models/test_user.py b/django/tests/core/iam/models/test_user.py index 67e5741f2..f0ffc8302 100644 --- a/django/tests/core/iam/models/test_user.py +++ b/django/tests/core/iam/models/test_user.py @@ -148,3 +148,28 @@ def test_queryset_delete_query_count_does_not_scale_with_user_count( User.objects.filter(pk__in=[u.pk for u in large]).delete() assert len(small_queries) == len(large_queries) + + +@pytest.mark.parametrize( + "factory_kwargs, expected", + [ + ({}, {"isStaff": False, "isSuperuser": False, "ownedWorkspaceLimit": 1}), + ( + {"owned_workspace_limit": 0}, + {"isStaff": False, "isSuperuser": False, "ownedWorkspaceLimit": 0}, + ), + ( + {"is_staff": True}, + {"isStaff": True, "isSuperuser": False, "ownedWorkspaceLimit": 1}, + ), + ( + {"superuser": True, "owned_workspace_limit": None}, + {"isStaff": True, "isSuperuser": True, "ownedWorkspaceLimit": None}, + ), + ], +) +def test_to_profile_claims_exposes_access_fields(factory_kwargs, expected): + claims = UserFactory(**factory_kwargs).to_profile_claims() + + assert {key: claims[key] for key in expected} == expected + assert "accountType" not in claims diff --git a/django/tests/hydroserver/test_oidc_auth.py b/django/tests/hydroserver/test_oidc_auth.py index 6532c97c4..bd450cd9c 100644 --- a/django/tests/hydroserver/test_oidc_auth.py +++ b/django/tests/hydroserver/test_oidc_auth.py @@ -517,7 +517,7 @@ def test_userinfo_includes_custom_claims_when_profile_scope_granted(oidc_client) expected = user.to_profile_claims() for key, value in expected.items(): assert claims[key] == value - assert claims["accountType"] == "admin" + assert claims["isSuperuser"] is True def test_userinfo_omits_custom_claims_without_profile_scope(oidc_client): @@ -526,7 +526,7 @@ def test_userinfo_omits_custom_claims_without_profile_scope(oidc_client): claims = adapter.get_claims("userinfo", user, oidc_client, ["openid"]) - assert "accountType" not in claims + assert "isSuperuser" not in claims assert "organization" not in claims @@ -546,7 +546,7 @@ def test_id_token_never_gets_custom_claims(oidc_client): "id_token", user, oidc_client, ["openid", "profile", "email"] ) - assert "accountType" not in claims + assert "isSuperuser" not in claims assert "organization" not in claims @@ -564,7 +564,10 @@ def test_userinfo_endpoint_returns_custom_claims(client, oidc_client): assert response.status_code == 200 body = response.json() assert body["firstName"] == "Jane" - assert body["accountType"] == "standard" + assert body["isStaff"] is False + assert body["isSuperuser"] is False + assert body["ownedWorkspaceLimit"] == 1 + assert "accountType" not in body def test_userinfo_endpoint_rejects_invalid_token(client): diff --git a/packages/hydroserver-ts/src/api/__tests__/user.service.spec.ts b/packages/hydroserver-ts/src/api/__tests__/user.service.spec.ts index 2ac325028..0f3cbe556 100644 --- a/packages/hydroserver-ts/src/api/__tests__/user.service.spec.ts +++ b/packages/hydroserver-ts/src/api/__tests__/user.service.spec.ts @@ -13,7 +13,6 @@ function makeClientWithCachedUser(user: User): HydroServer { function makeStandardUser(email = 'collaborator@example.com'): User { const user = new User() user.email = email - user.accountType = 'standard' return user } @@ -27,7 +26,7 @@ function makeWorkspace(overrides: Partial = {}): Workspace { describe('UserService.can', () => { it('allows an admin regardless of role permissions', async () => { const admin = makeStandardUser('admin@example.com') - admin.accountType = 'admin' + admin.isSuperuser = true const client = makeClientWithCachedUser(admin) const workspace = makeWorkspace() diff --git a/packages/hydroserver-ts/src/api/services/user.service.ts b/packages/hydroserver-ts/src/api/services/user.service.ts index f271efd20..4dbb5f9b0 100644 --- a/packages/hydroserver-ts/src/api/services/user.service.ts +++ b/packages/hydroserver-ts/src/api/services/user.service.ts @@ -112,7 +112,7 @@ export class UserService { * * OIDC mode: fetched once from the OIDC UserInfo endpoint and cached. * HydroServer's OIDC provider adds HydroServer-specific claims - * (organization, accountType, etc.) to that response for clients + * (organization, isSuperuser, etc.) to that response for clients * requesting the `profile` scope (see core/iam/auth/oidc_adapter.py). * The access token is attached automatically by requestInterceptor via * the accessTokenProvider registered in runtime.ts. @@ -159,7 +159,7 @@ export class UserService { } function isAdmin(user: User | null): boolean { - return (user?.accountType as string) === 'admin' + return user?.isSuperuser === true } function isOwner( diff --git a/packages/hydroserver-ts/src/types/index.ts b/packages/hydroserver-ts/src/types/index.ts index 6d28bccac..88bea0fd8 100644 --- a/packages/hydroserver-ts/src/types/index.ts +++ b/packages/hydroserver-ts/src/types/index.ts @@ -399,7 +399,10 @@ export class User { organization?: Organization | null type: string link: string - accountType: 'admin' | 'standard' | 'limited' + isStaff: boolean + isSuperuser: boolean + /** `null` means unlimited; `0` means the user can't own workspaces. */ + ownedWorkspaceLimit: number | null hydroShareConnected: boolean constructor() { @@ -413,7 +416,9 @@ export class User { this.address = '' this.type = '' this.link = '' - this.accountType = 'standard' + this.isStaff = false + this.isSuperuser = false + this.ownedWorkspaceLimit = 1 this.hydroShareConnected = false } } diff --git a/packages/qc-utils/src/types/index.ts b/packages/qc-utils/src/types/index.ts index ba9fda511..f0813d606 100644 --- a/packages/qc-utils/src/types/index.ts +++ b/packages/qc-utils/src/types/index.ts @@ -511,7 +511,10 @@ export class User { organization?: Organization | null; type: string; link: string; - accountType: "admin" | "standard" | "limited"; + isStaff: boolean; + isSuperuser: boolean; + /** `null` means unlimited; `0` means the user can't own workspaces. */ + ownedWorkspaceLimit: number | null; hydroShareConnected: boolean; constructor() { @@ -525,7 +528,9 @@ export class User { this.address = ""; this.type = ""; this.link = ""; - this.accountType = "standard"; + this.isStaff = false; + this.isSuperuser = false; + this.ownedWorkspaceLimit = 1; this.hydroShareConnected = false; } } From d0c34031440222bfc8282a903d45a49be243a31a Mon Sep 17 00:00:00 2001 From: Ken Lippold Date: Thu, 15 Oct 2026 09:18:44 -0700 Subject: [PATCH 3/3] Sync QC app lockfile with Vue version --- apps/qc-app/package-lock.json | 161 +++++++++++++++++----------------- 1 file changed, 81 insertions(+), 80 deletions(-) diff --git a/apps/qc-app/package-lock.json b/apps/qc-app/package-lock.json index 061212eec..8f58fbad5 100644 --- a/apps/qc-app/package-lock.json +++ b/apps/qc-app/package-lock.json @@ -21,7 +21,7 @@ "pinia-plugin-persistedstate": "^4.7.1", "plotly.js-dist": "^3.5.1", "tslib": "^2.8.1", - "vue": "^3.5.35", + "vue": "^3.5.40", "vue-router": "^5.1.0", "vuetify": "^4.1.1" }, @@ -60,12 +60,15 @@ "name": "@hydroserver/client", "version": "1.0.0", "license": "BSD-3-Clause", + "dependencies": { + "oidc-client-ts": "^3.5.0", + "p-limit": "^6.2.0" + }, "devDependencies": { "@types/node": "^24.1.0", "@vitest/coverage-v8": "^4.1.0", "jsdom": "^22.1.0", "openapi-zod-client": "^1.18.3", - "p-limit": "^6.2.0", "tsx": "^4.16.2", "typescript": "^5.8.3", "vite": "^7.3.1", @@ -221,12 +224,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -236,9 +239,9 @@ } }, "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -1935,53 +1938,53 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.38", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz", - "integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.44.tgz", + "integrity": "sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.7", - "@vue/shared": "3.5.38", + "@babel/parser": "^7.29.9", + "@vue/shared": "3.5.44", "entities": "^7.0.1", "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.38", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz", - "integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.44.tgz", + "integrity": "sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.38", - "@vue/shared": "3.5.38" + "@vue/compiler-core": "3.5.44", + "@vue/shared": "3.5.44" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.38", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz", - "integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.44.tgz", + "integrity": "sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.7", - "@vue/compiler-core": "3.5.38", - "@vue/compiler-dom": "3.5.38", - "@vue/compiler-ssr": "3.5.38", - "@vue/shared": "3.5.38", + "@babel/parser": "^7.29.9", + "@vue/compiler-core": "3.5.44", + "@vue/compiler-dom": "3.5.44", + "@vue/compiler-ssr": "3.5.44", + "@vue/shared": "3.5.44", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", - "postcss": "^8.5.15", - "source-map-js": "^1.2.1" + "postcss": "^8.5.29", + "source-map-js": "^1.2.2" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.38", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz", - "integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.44.tgz", + "integrity": "sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.38", - "@vue/shared": "3.5.38" + "@vue/compiler-dom": "3.5.44", + "@vue/shared": "3.5.44" } }, "node_modules/@vue/devtools-api": { @@ -2034,53 +2037,51 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.38", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz", - "integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.44.tgz", + "integrity": "sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.38" + "@vue/shared": "3.5.44" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.38", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.38.tgz", - "integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.44.tgz", + "integrity": "sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.38", - "@vue/shared": "3.5.38" + "@vue/reactivity": "3.5.44", + "@vue/shared": "3.5.44" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.38", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz", - "integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.44.tgz", + "integrity": "sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.38", - "@vue/runtime-core": "3.5.38", - "@vue/shared": "3.5.38", + "@vue/reactivity": "3.5.44", + "@vue/runtime-core": "3.5.44", + "@vue/shared": "3.5.44", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.38", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.38.tgz", - "integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.44.tgz", + "integrity": "sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.38", - "@vue/shared": "3.5.38" - }, - "peerDependencies": { - "vue": "3.5.38" + "@vue/compiler-ssr": "3.5.44", + "@vue/runtime-dom": "3.5.44", + "@vue/shared": "3.5.44" } }, "node_modules/@vue/shared": { - "version": "3.5.38", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz", - "integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.44.tgz", + "integrity": "sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg==", "license": "MIT" }, "node_modules/@vue/test-utils": { @@ -4139,9 +4140,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.13", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.13.tgz", - "integrity": "sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "funding": [ { "type": "github", @@ -4577,9 +4578,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", + "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", "funding": [ { "type": "opencollective", @@ -4596,9 +4597,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -4900,9 +4901,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -5598,16 +5599,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.38", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.38.tgz", - "integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==", + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.44.tgz", + "integrity": "sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.38", - "@vue/compiler-sfc": "3.5.38", - "@vue/runtime-dom": "3.5.38", - "@vue/server-renderer": "3.5.38", - "@vue/shared": "3.5.38" + "@vue/compiler-dom": "3.5.44", + "@vue/compiler-sfc": "3.5.44", + "@vue/runtime-dom": "3.5.44", + "@vue/server-renderer": "3.5.44", + "@vue/shared": "3.5.44" }, "peerDependencies": { "typescript": "*"