From 9ce7a67f58db4701c2eabf4fb72efc11806ffad9 Mon Sep 17 00:00:00 2001 From: Lectom Date: Thu, 30 Apr 2026 09:33:39 +0900 Subject: [PATCH] feat: integrate orgfront and expose internal ids --- .gitea/workflows/build_RC.yml | 10 + .gitea/workflows/code_check.yml | 202 +- .gitea/workflows/staging_code_pull.yml | 3 +- .gitea/workflows/staging_release.yml | 5 + .gitignore | 5 + .../src/components/layout/AppLayout.tsx | 10 +- .../tenants/routes/TenantListPage.tsx | 13 +- .../src/features/users/UserCreatePage.tsx | 2 +- .../src/features/users/UserDetailPage.tsx | 65 +- .../src/features/users/UserListPage.tsx | 13 +- .../src/features/users/orgChartPicker.test.ts | 56 +- .../src/features/users/orgChartPicker.ts | 93 +- adminfront/tests/auth.spec.ts | 10 + adminfront/tests/tenants.spec.ts | 5 +- adminfront/tests/users.spec.ts | 34 + adminfront/vite.config.ts | 2 +- deploy/templates/adminfront/vite.config.ts | 2 +- deploy/templates/docker-compose.yaml | 14 + deploy/templates/orgfront/auth.ts | 23 + deploy/templates/orgfront/vite.config.ts | 36 + docker-compose.yaml | 4 +- docker/docker-compose.staging.template.yaml | 14 + docker/staging_pull_compose.template.yaml | 19 + .../issue-663-hanmac-family-orgfront-sync.md | 43 + orgfront | 1 - orgfront/.dockerignore | 8 + orgfront/.gitignore | 24 + orgfront/Dockerfile | 20 + orgfront/Dockerfile2 | 20 + orgfront/README.md | 106 + orgfront/biome.json | 29 + orgfront/docker-compose.yml | 19 + .../docs/orgchart-embedding-token-design.md | 125 + orgfront/hydra-rp-dummy.py | 188 + orgfront/index.html | 13 + orgfront/package-lock.json | 3220 +++++++++++++++++ orgfront/package.json | 52 + orgfront/playwright.config.ts | 65 + orgfront/postcss.config.js | 6 + orgfront/public/vite.svg | 1 + orgfront/scripts/runtime-mode.sh | 26 + orgfront/src/app/queryClient.ts | 11 + orgfront/src/app/routes.tsx | 46 + orgfront/src/assets/react.svg | 1 + .../components/common/ForbiddenMessage.tsx | 51 + .../components/common/LanguageSelector.tsx | 53 + orgfront/src/components/layout/AppLayout.tsx | 567 +++ orgfront/src/components/ui/avatar.tsx | 47 + orgfront/src/components/ui/badge.tsx | 39 + orgfront/src/components/ui/button.tsx | 56 + orgfront/src/components/ui/card.tsx | 72 + orgfront/src/components/ui/copy-button.tsx | 75 + orgfront/src/components/ui/input.tsx | 24 + orgfront/src/components/ui/label.tsx | 19 + orgfront/src/components/ui/scroll-area.tsx | 44 + orgfront/src/components/ui/separator.tsx | 16 + orgfront/src/components/ui/switch.tsx | 26 + orgfront/src/components/ui/table.tsx | 113 + orgfront/src/components/ui/textarea.tsx | 23 + orgfront/src/components/ui/toaster.tsx | 35 + orgfront/src/components/ui/use-toast.ts | 42 + orgfront/src/features/audit/AuditLogsPage.tsx | 422 +++ .../src/features/auth/AuthCallbackPage.tsx | 35 + orgfront/src/features/auth/AuthGuard.tsx | 60 + orgfront/src/features/auth/AuthPage.tsx | 111 + orgfront/src/features/auth/LoginPage.tsx | 126 + orgfront/src/features/auth/authApi.ts | 24 + .../features/clients/ClientConsentsPage.tsx | 600 +++ .../features/clients/ClientDetailsPage.tsx | 540 +++ .../features/clients/ClientGeneralPage.tsx | 1645 +++++++++ orgfront/src/features/clients/ClientsPage.tsx | 527 +++ .../clients/routes/ClientFederationPage.tsx | 306 ++ .../src/features/dashboard/DashboardPage.tsx | 294 ++ orgfront/src/features/orgchart/pickerTree.ts | 135 + orgfront/src/features/orgchart/pickerTypes.ts | 98 + .../features/orgchart/routes/OrgChartPage.tsx | 881 +++++ .../orgchart/routes/OrgFrontLayout.tsx | 48 + .../routes/OrgPickerEmbedPreviewPage.tsx | 218 ++ .../orgchart/routes/OrgPickerPage.tsx | 709 ++++ orgfront/src/features/orgchart/userDisplay.ts | 63 + orgfront/src/features/profile/ProfilePage.tsx | 219 ++ .../profile/ProfileTenantSwitcher.tsx | 92 + orgfront/src/index.css | 95 + orgfront/src/lib/adminApi.ts | 724 ++++ orgfront/src/lib/apiClient.ts | 52 + orgfront/src/lib/auth.ts | 22 + orgfront/src/lib/devApi.ts | 315 ++ orgfront/src/lib/i18n.ts | 148 + orgfront/src/lib/role.ts | 27 + orgfront/src/lib/sessionSliding.ts | 76 + orgfront/src/lib/tenantTree.ts | 86 + orgfront/src/lib/utils.ts | 6 + orgfront/src/locales/en.toml | 1793 +++++++++ orgfront/src/locales/ko.toml | 1789 +++++++++ orgfront/src/locales/template.toml | 1778 +++++++++ orgfront/src/main.tsx | 27 + orgfront/tailwind.config.ts | 67 + orgfront/tests/clients.spec.ts | 39 + orgfront/tests/devfront-audit.spec.ts | 120 + .../tests/devfront-clients-lifecycle.spec.ts | 374 ++ orgfront/tests/devfront-consents.spec.ts | 45 + .../tests/devfront-role-switch-report.spec.ts | 169 + orgfront/tests/devfront-security.spec.ts | 122 + orgfront/tests/devfront-tenant-switch.spec.ts | 116 + orgfront/tests/example.spec.ts | 8 + orgfront/tests/helpers/devfront-fixtures.ts | 513 +++ orgfront/tests/helpers/evidence.ts | 24 + orgfront/tests/light-theme.spec.ts | 38 + orgfront/tests/orgchart-pan-zoom.spec.ts | 127 + orgfront/tests/orgchart-picker.spec.ts | 599 +++ orgfront/tests/orgchart-vector-render.spec.ts | 379 ++ orgfront/tsconfig.app.json | 28 + orgfront/tsconfig.json | 7 + orgfront/tsconfig.node.json | 26 + orgfront/vite.config.ts | 58 + test/orgfront_integration_policy_test.sh | 109 + 116 files changed, 22992 insertions(+), 33 deletions(-) create mode 100644 deploy/templates/orgfront/auth.ts create mode 100644 deploy/templates/orgfront/vite.config.ts create mode 100644 docs/trouble-shooting/issue-663-hanmac-family-orgfront-sync.md delete mode 160000 orgfront create mode 100644 orgfront/.dockerignore create mode 100644 orgfront/.gitignore create mode 100644 orgfront/Dockerfile create mode 100644 orgfront/Dockerfile2 create mode 100644 orgfront/README.md create mode 100644 orgfront/biome.json create mode 100644 orgfront/docker-compose.yml create mode 100644 orgfront/docs/orgchart-embedding-token-design.md create mode 100644 orgfront/hydra-rp-dummy.py create mode 100644 orgfront/index.html create mode 100644 orgfront/package-lock.json create mode 100644 orgfront/package.json create mode 100644 orgfront/playwright.config.ts create mode 100644 orgfront/postcss.config.js create mode 100644 orgfront/public/vite.svg create mode 100644 orgfront/scripts/runtime-mode.sh create mode 100644 orgfront/src/app/queryClient.ts create mode 100644 orgfront/src/app/routes.tsx create mode 100644 orgfront/src/assets/react.svg create mode 100644 orgfront/src/components/common/ForbiddenMessage.tsx create mode 100644 orgfront/src/components/common/LanguageSelector.tsx create mode 100644 orgfront/src/components/layout/AppLayout.tsx create mode 100644 orgfront/src/components/ui/avatar.tsx create mode 100644 orgfront/src/components/ui/badge.tsx create mode 100644 orgfront/src/components/ui/button.tsx create mode 100644 orgfront/src/components/ui/card.tsx create mode 100644 orgfront/src/components/ui/copy-button.tsx create mode 100644 orgfront/src/components/ui/input.tsx create mode 100644 orgfront/src/components/ui/label.tsx create mode 100644 orgfront/src/components/ui/scroll-area.tsx create mode 100644 orgfront/src/components/ui/separator.tsx create mode 100644 orgfront/src/components/ui/switch.tsx create mode 100644 orgfront/src/components/ui/table.tsx create mode 100644 orgfront/src/components/ui/textarea.tsx create mode 100644 orgfront/src/components/ui/toaster.tsx create mode 100644 orgfront/src/components/ui/use-toast.ts create mode 100644 orgfront/src/features/audit/AuditLogsPage.tsx create mode 100644 orgfront/src/features/auth/AuthCallbackPage.tsx create mode 100644 orgfront/src/features/auth/AuthGuard.tsx create mode 100644 orgfront/src/features/auth/AuthPage.tsx create mode 100644 orgfront/src/features/auth/LoginPage.tsx create mode 100644 orgfront/src/features/auth/authApi.ts create mode 100644 orgfront/src/features/clients/ClientConsentsPage.tsx create mode 100644 orgfront/src/features/clients/ClientDetailsPage.tsx create mode 100644 orgfront/src/features/clients/ClientGeneralPage.tsx create mode 100644 orgfront/src/features/clients/ClientsPage.tsx create mode 100644 orgfront/src/features/clients/routes/ClientFederationPage.tsx create mode 100644 orgfront/src/features/dashboard/DashboardPage.tsx create mode 100644 orgfront/src/features/orgchart/pickerTree.ts create mode 100644 orgfront/src/features/orgchart/pickerTypes.ts create mode 100644 orgfront/src/features/orgchart/routes/OrgChartPage.tsx create mode 100644 orgfront/src/features/orgchart/routes/OrgFrontLayout.tsx create mode 100644 orgfront/src/features/orgchart/routes/OrgPickerEmbedPreviewPage.tsx create mode 100644 orgfront/src/features/orgchart/routes/OrgPickerPage.tsx create mode 100644 orgfront/src/features/orgchart/userDisplay.ts create mode 100644 orgfront/src/features/profile/ProfilePage.tsx create mode 100644 orgfront/src/features/profile/ProfileTenantSwitcher.tsx create mode 100644 orgfront/src/index.css create mode 100644 orgfront/src/lib/adminApi.ts create mode 100644 orgfront/src/lib/apiClient.ts create mode 100644 orgfront/src/lib/auth.ts create mode 100644 orgfront/src/lib/devApi.ts create mode 100644 orgfront/src/lib/i18n.ts create mode 100644 orgfront/src/lib/role.ts create mode 100644 orgfront/src/lib/sessionSliding.ts create mode 100644 orgfront/src/lib/tenantTree.ts create mode 100644 orgfront/src/lib/utils.ts create mode 100644 orgfront/src/locales/en.toml create mode 100644 orgfront/src/locales/ko.toml create mode 100644 orgfront/src/locales/template.toml create mode 100644 orgfront/src/main.tsx create mode 100644 orgfront/tailwind.config.ts create mode 100644 orgfront/tests/clients.spec.ts create mode 100644 orgfront/tests/devfront-audit.spec.ts create mode 100644 orgfront/tests/devfront-clients-lifecycle.spec.ts create mode 100644 orgfront/tests/devfront-consents.spec.ts create mode 100644 orgfront/tests/devfront-role-switch-report.spec.ts create mode 100644 orgfront/tests/devfront-security.spec.ts create mode 100644 orgfront/tests/devfront-tenant-switch.spec.ts create mode 100644 orgfront/tests/example.spec.ts create mode 100644 orgfront/tests/helpers/devfront-fixtures.ts create mode 100644 orgfront/tests/helpers/evidence.ts create mode 100644 orgfront/tests/light-theme.spec.ts create mode 100644 orgfront/tests/orgchart-pan-zoom.spec.ts create mode 100644 orgfront/tests/orgchart-picker.spec.ts create mode 100644 orgfront/tests/orgchart-vector-render.spec.ts create mode 100644 orgfront/tsconfig.app.json create mode 100644 orgfront/tsconfig.json create mode 100644 orgfront/tsconfig.node.json create mode 100644 orgfront/vite.config.ts create mode 100644 test/orgfront_integration_policy_test.sh diff --git a/.gitea/workflows/build_RC.yml b/.gitea/workflows/build_RC.yml index c0709781..e81069fa 100644 --- a/.gitea/workflows/build_RC.yml +++ b/.gitea/workflows/build_RC.yml @@ -106,6 +106,16 @@ jobs: provenance: false sbom: false + - name: Build and push orgfront RC image + uses: docker/build-push-action@v5 + with: + context: ./orgfront + file: ./orgfront/Dockerfile + push: true + tags: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/orgfront:${{ steps.rc_calculator.outputs.new_rc_tag }} + provenance: false + sbom: false + - name: Build and push userfront RC image uses: docker/build-push-action@v5 with: diff --git a/.gitea/workflows/code_check.yml b/.gitea/workflows/code_check.yml index df49d094..1970cd12 100644 --- a/.gitea/workflows/code_check.yml +++ b/.gitea/workflows/code_check.yml @@ -10,7 +10,7 @@ on: workflow_dispatch: inputs: run_lint: - description: "Run lint/format checks for Go, Flutter, adminfront, devfront" + description: "Run lint/format checks for Go, Flutter, adminfront, devfront, orgfront" required: true type: boolean default: true @@ -39,6 +39,11 @@ on: required: true type: boolean default: true + run_orgfront_tests: + description: "Run orgfront Playwright tests" + required: true + type: boolean + default: true jobs: lint: @@ -56,6 +61,7 @@ jobs: cache-dependency-path: | adminfront/package-lock.json devfront/package-lock.json + orgfront/package-lock.json - name: i18n resource check run: | @@ -104,6 +110,17 @@ jobs: npx biome check . --formatter-enabled=false --organize-imports-enabled=false npx biome check . --linter-enabled=false --organize-imports-enabled=false + - name: Install orgfront dependencies + run: | + cd orgfront + npm ci + + - name: Biome check orgfront (lint + format) + run: | + cd orgfront + npx biome check . --formatter-enabled=false --organize-imports-enabled=false + npx biome check . --linter-enabled=false --organize-imports-enabled=false + - name: Lint Go backend run: | docker run --rm \ @@ -809,3 +826,186 @@ jobs: devfront/playwright-report devfront/test-results if-no-files-found: ignore + + orgfront-tests: + needs: lint + if: ${{ always() && (github.event_name != 'workflow_dispatch' || inputs.run_orgfront_tests == true) }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + cache: "npm" + cache-dependency-path: orgfront/package-lock.json + + - name: Get Playwright version + id: playwright-version + run: | + cd orgfront + echo "version=$(npm list @playwright/test | grep @playwright/test | awk -F@ '{print $NF}')" >> "$GITHUB_OUTPUT" + + - name: Cache Playwright Browsers + uses: actions/cache@v4 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }} + restore-keys: | + ${{ runner.os }}-playwright- + + - name: Install orgfront dependencies + run: | + mkdir -p reports + set +e + cd orgfront + npm ci 2>&1 | tee ../reports/orgfront-install.log + install_exit_code=${PIPESTATUS[0]} + cd .. + set -e + + if [ "$install_exit_code" -ne 0 ]; then + { + echo "# OrgFront Test Failure Report" + echo + echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`" + echo "- Job: \`orgfront-tests\`" + echo "- Reason: \`Dependency install failed\`" + echo "- Exit Code: \`$install_exit_code\`" + echo + echo "## Command" + echo "\`cd orgfront && npm ci\`" + echo + echo "## Install Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/orgfront-install.log + echo '```' + } > reports/orgfront-test-failure-report.md + exit 1 + fi + + - name: Provision browsers for orgfront tests + run: | + set +e + cd orgfront + npx playwright install --with-deps 2>&1 | tee ../reports/orgfront-provision.log + provision_exit_code=${PIPESTATUS[0]} + cd .. + set -e + + if [ "$provision_exit_code" -ne 0 ]; then + { + echo "# OrgFront Test Failure Report" + echo + echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`" + echo "- Job: \`orgfront-tests\`" + echo "- Reason: \`Browser provisioning failed\`" + echo "- Exit Code: \`$provision_exit_code\`" + echo + echo "## Command" + echo "\`cd orgfront && npx playwright install --with-deps\`" + echo + echo "## Provision Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/orgfront-provision.log + echo '```' + } > reports/orgfront-test-failure-report.md + exit 1 + fi + + - name: Run orgfront tests + env: + PLAYWRIGHT_WORKERS: 2 + run: | + mkdir -p reports + set +e + cd orgfront + npm test 2>&1 | tee ../reports/orgfront-test.log + test_exit_code=${PIPESTATUS[0]} + cd .. + set -e + + if [ "$test_exit_code" -ne 0 ]; then + { + echo "# OrgFront Test Failure Report" + echo + echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`" + echo "- Job: \`orgfront-tests\`" + echo "- Exit Code: \`$test_exit_code\`" + echo + echo "## Commands" + echo "1. \`cd orgfront\`" + echo "2. \`npm ci\`" + echo "3. \`npx playwright install --with-deps\`" + echo "4. \`npm test\`" + echo + echo "## Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/orgfront-test.log + echo '```' + } > reports/orgfront-test-failure-report.md + fi + + exit "$test_exit_code" + + - name: Ensure orgfront failure report exists + if: ${{ failure() }} + run: | + mkdir -p reports + if [ -f reports/orgfront-test-failure-report.md ]; then + exit 0 + fi + + { + echo "# OrgFront Test Failure Report" + echo + echo "- Workflow: \`${GITHUB_WORKFLOW:-Code Check}\`" + echo "- Job: \`orgfront-tests\`" + echo "- Reason: \`Job failed before detailed report generation\`" + echo + if [ -f reports/orgfront-install.log ]; then + echo "## Install Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/orgfront-install.log + echo '```' + echo + fi + if [ -f reports/orgfront-provision.log ]; then + echo "## Provision Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/orgfront-provision.log + echo '```' + echo + fi + if [ -f reports/orgfront-test.log ]; then + echo "## Test Log Tail (last 200 lines)" + echo '```text' + tail -n 200 reports/orgfront-test.log + echo '```' + fi + } > reports/orgfront-test-failure-report.md + + - name: Publish orgfront failure summary + if: ${{ failure() }} + run: | + if [ -f reports/orgfront-test-failure-report.md ]; then + cat reports/orgfront-test-failure-report.md >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Upload orgfront failure report artifact + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + continue-on-error: true + with: + name: orgfront-test-failure-report + path: | + reports/orgfront-test-failure-report.md + reports/orgfront-install.log + reports/orgfront-provision.log + reports/orgfront-test.log + orgfront/playwright-report + orgfront/test-results + if-no-files-found: ignore diff --git a/.gitea/workflows/staging_code_pull.yml b/.gitea/workflows/staging_code_pull.yml index 261175ae..2abd5158 100644 --- a/.gitea/workflows/staging_code_pull.yml +++ b/.gitea/workflows/staging_code_pull.yml @@ -63,6 +63,7 @@ jobs: BACKEND_PORT=${{ vars.BACKEND_PORT }} ADMINFRONT_PORT=${{ vars.ADMINFRONT_PORT }} DEVFRONT_PORT=${{ vars.DEVFRONT_PORT }} + ORGFRONT_PORT=${{ vars.ORGFRONT_PORT }} USERFRONT_PORT=${{ vars.USERFRONT_PORT }} OATHKEEPER_API_URL=${{ vars.OATHKEEPER_API_URL }} @@ -90,7 +91,7 @@ jobs: USERFRONT_URL=${{ vars.USERFRONT_URL }} ADMINFRONT_URL=${{ vars.ADMINFRONT_URL }} DEVFRONT_URL=${{ vars.DEVFRONT_URL }} - VITE_ORGCHART_URL=${{ vars.VITE_ORGCHART_URL }} + ORGFRONT_URL=${{ vars.ORGFRONT_URL }} BACKEND_PUBLIC_URL=${{ vars.BACKEND_URL }} BACKEND_URL=${{ vars.BACKEND_URL }} OATHKEEPER_PUBLIC_URL=${{ vars.OATHKEEPER_PUBLIC_URL }} diff --git a/.gitea/workflows/staging_release.yml b/.gitea/workflows/staging_release.yml index 2b5a561b..a3e84d53 100644 --- a/.gitea/workflows/staging_release.yml +++ b/.gitea/workflows/staging_release.yml @@ -27,6 +27,7 @@ jobs: USERFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/userfront ADMINFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/adminfront DEVFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/devfront + ORGFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/orgfront # Staging-specific variables DEPLOY_PATH: ${{ vars.STAGE_DEPLOY_PATH }} @@ -72,6 +73,7 @@ jobs: BACKEND_PORT=${{ vars.BACKEND_PORT }} ADMINFRONT_PORT=${{ vars.ADMINFRONT_PORT }} DEVFRONT_PORT=${{ vars.DEVFRONT_PORT }} + ORGFRONT_PORT=${{ vars.ORGFRONT_PORT }} USERFRONT_PORT=${{ vars.USERFRONT_PORT }} OATHKEEPER_API_URL=${{ vars.OATHKEEPER_API_URL }} @@ -97,6 +99,7 @@ jobs: ADMIN_EMAIL=${{ vars.ADMIN_EMAIL }} ADMIN_PASSWORD=${{ secrets.STG_ADMIN_PASSWORD }} USERFRONT_URL=${{ vars.USERFRONT_URL }} + ORGFRONT_URL=${{ vars.ORGFRONT_URL }} BACKEND_PUBLIC_URL=${{ vars.BACKEND_URL }} BACKEND_URL=${{ vars.BACKEND_URL }} OATHKEEPER_PUBLIC_URL=${{ vars.OATHKEEPER_PUBLIC_URL }} @@ -132,6 +135,7 @@ jobs: VITE_OIDC_AUTHORITY=${{ vars.VITE_OIDC_AUTHORITY }} ADMINFRONT_CALLBACK_URLS=${{ vars.ADMINFRONT_CALLBACK_URLS }} DEVFRONT_CALLBACK_URLS=${{ vars.DEVFRONT_CALLBACK_URLS }} + ORGFRONT_CALLBACK_URLS=${{ vars.ORGFRONT_CALLBACK_URLS }} # OATHKEEPER_INTROSPECT_CLIENT_ID=${{ vars.OATHKEEPER_INTROSPECT_CLIENT_ID }} # OATHKEEPER_INTROSPECT_CLIENT_SECRET=${{ secrets.STG_OATHKEEPER_INTROSPECT_CLIENT_SECRET }} EOF @@ -161,6 +165,7 @@ jobs: export USERFRONT_IMAGE_NAME='${USERFRONT_IMAGE_NAME}'; \ export ADMINFRONT_IMAGE_NAME='${ADMINFRONT_IMAGE_NAME}'; \ export DEVFRONT_IMAGE_NAME='${DEVFRONT_IMAGE_NAME}'; \ + export ORGFRONT_IMAGE_NAME='${ORGFRONT_IMAGE_NAME}'; \ export IMAGE_TAG='${IMAGE_TAG}'; \ export HARBOR_ENDPOINT='${HARBOR_ENDPOINT}'; \ export HARBOR_ROBOT_ACCOUNT='${HARBOR_ROBOT_ACCOUNT}'; \ diff --git a/.gitignore b/.gitignore index 7dd6fb9e..f2a4f6f5 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,11 @@ userfront/.env # Frontend test artifacts adminfront/test-results/ +adminfront/test-results.nobody-backup/ devfront/test-results/ +orgfront/test-results/ adminfront/playwright-report/ devfront/playwright-report/ +orgfront/playwright-report/ +orgfront/node_modules/ +orgfront/dist/ diff --git a/adminfront/src/components/layout/AppLayout.tsx b/adminfront/src/components/layout/AppLayout.tsx index 85eafa13..4f71fe3d 100644 --- a/adminfront/src/components/layout/AppLayout.tsx +++ b/adminfront/src/components/layout/AppLayout.tsx @@ -24,6 +24,7 @@ import { shouldAttemptSlidingSessionRenew, shouldAttemptUnlimitedSessionRenew, } from "../../lib/sessionSliding"; +import { buildAuthenticatedOrgChartUrl } from "../../features/users/orgChartPicker"; import LanguageSelector from "../common/LanguageSelector"; import RoleSwitcher from "./RoleSwitcher"; @@ -114,6 +115,9 @@ function AppLayout() { const isSuperAdmin = isTest || effectiveRole === "super_admin"; const isTenantAdmin = effectiveRole === "tenant_admin"; const manageableCount = profile?.manageableTenants?.length ?? 0; + const orgfrontUrl = buildAuthenticatedOrgChartUrl( + import.meta.env.ORGFRONT_URL || "http://localhost:5175", + ); const filteredItems = items.filter((item) => { if (isTest) return true; @@ -129,7 +133,7 @@ function AppLayout() { }); filteredItems.splice(2, 0, { label: "ui.admin.nav.org_chart", - to: import.meta.env.VITE_ORGCHART_URL || "http://localhost:5175", + to: orgfrontUrl, icon: Network, isExternal: true, }); @@ -152,7 +156,7 @@ function AppLayout() { 0, { label: "ui.admin.nav.org_chart", - to: import.meta.env.VITE_ORGCHART_URL || "http://localhost:5175", + to: orgfrontUrl, icon: Network, isExternal: true, }, @@ -161,7 +165,7 @@ function AppLayout() { // 일반 사용자(Tenant Member)도 조직도 메뉴를 볼 수 있도록 추가합니다. filteredItems.splice(1, 0, { label: "ui.admin.nav.org_chart", - to: import.meta.env.VITE_ORGCHART_URL || "http://localhost:5175", + to: orgfrontUrl, icon: Network, isExternal: true, }); diff --git a/adminfront/src/features/tenants/routes/TenantListPage.tsx b/adminfront/src/features/tenants/routes/TenantListPage.tsx index 799a133c..5067d645 100644 --- a/adminfront/src/features/tenants/routes/TenantListPage.tsx +++ b/adminfront/src/features/tenants/routes/TenantListPage.tsx @@ -439,6 +439,9 @@ function TenantListPage() { } /> + + {t("ui.admin.tenants.table.id", "ID")} + {t("ui.admin.tenants.table.name", "NAME")} @@ -465,7 +468,7 @@ function TenantListPage() { {query.isLoading && ( - + {t("msg.common.loading", "로딩 중...")} @@ -473,7 +476,7 @@ function TenantListPage() { {!query.isLoading && tenants.length === 0 && ( {t( @@ -493,6 +496,12 @@ function TenantListPage() { } /> + + {tenant.id} + {tenant.name} diff --git a/adminfront/src/features/users/UserCreatePage.tsx b/adminfront/src/features/users/UserCreatePage.tsx index c634cb35..df651450 100644 --- a/adminfront/src/features/users/UserCreatePage.tsx +++ b/adminfront/src/features/users/UserCreatePage.tsx @@ -236,7 +236,7 @@ function UserCreatePage() { }); const pickerUrl = buildAuthenticatedOrgChartTenantPickerUrl( - import.meta.env.VITE_ORGCHART_URL, + import.meta.env.ORGFRONT_URL, { tenantId: userType === "hanmac" ? hanmacFamilyTenantId : undefined, }, diff --git a/adminfront/src/features/users/UserDetailPage.tsx b/adminfront/src/features/users/UserDetailPage.tsx index c6e7211b..ef4c5ea4 100644 --- a/adminfront/src/features/users/UserDetailPage.tsx +++ b/adminfront/src/features/users/UserDetailPage.tsx @@ -73,6 +73,8 @@ import { type OrgChartTenantSelection, buildAuthenticatedOrgChartTenantPickerUrl, filterNonHanmacFamilyTenants, + isHanmacFamilyTenant, + isHanmacFamilyUser, parseOrgChartTenantSelection, } from "./orgChartPicker"; @@ -369,7 +371,10 @@ function UserDetailPage() { queryKey: ["tenants", { limit: 100 }], queryFn: () => fetchTenants(100, 0), }); - const tenants = tenantsData?.items ?? []; + const tenants = React.useMemo( + () => tenantsData?.items ?? [], + [tenantsData?.items], + ); const rpHistoryQuery = useQuery({ queryKey: ["user-rp-history", userId], @@ -498,7 +503,7 @@ function UserDetailPage() { ); const pickerUrl = buildAuthenticatedOrgChartTenantPickerUrl( - import.meta.env.VITE_ORGCHART_URL, + import.meta.env.ORGFRONT_URL, { tenantId: userType === "hanmac" ? hanmacFamilyTenantId : undefined, }, @@ -642,15 +647,33 @@ function UserDetailPage() { Record >) || {}, }); + const isUserHanmacFamily = isHanmacFamilyUser( + user, + tenants, + hanmacFamilyTenantId, + ); const resolvedUserType = metadata.userType === "personal" || user.companyCode === "personal" ? "personal" - : metadata.hanmacFamily === true + : isUserHanmacFamily ? "hanmac" : "external"; setUserType(resolvedUserType); setIsHanmacFamily(resolvedUserType === "hanmac"); + const familyFallbackTenants = [ + ...(user.joinedTenants ?? []), + ...(user.tenant ? [user.tenant] : []), + ].filter( + (tenant, index, allTenants) => + allTenants.findIndex((item) => item.id === tenant.id) === + index && + isHanmacFamilyTenant( + tenant, + tenants, + hanmacFamilyTenantId, + ), + ); setAdditionalAppointments( Array.isArray(rawAppointments) ? (rawAppointments as UserAppointment[]).map( @@ -659,22 +682,38 @@ function UserDetailPage() { draftId: createDraftId(), }), ) - : metadata.hanmacFamily === true && fallbackAppointment - ? [ - { + : isUserHanmacFamily + ? familyFallbackTenants.length > 0 + ? familyFallbackTenants.map((tenant) => ({ draftId: createDraftId(), - tenantId: fallbackAppointment.id, - tenantName: fallbackAppointment.name, - tenantSlug: fallbackAppointment.slug, - isOwner: metadata.primaryTenantIsOwner === true, + tenantId: tenant.id, + tenantName: tenant.name, + tenantSlug: tenant.slug, + isOwner: + metadata.primaryTenantIsOwner === true && + tenant.id === fallbackAppointment?.id, jobTitle: user.jobTitle, position: user.position, - }, - ] + })) + : fallbackAppointment + ? [ + { + draftId: createDraftId(), + tenantId: fallbackAppointment.id, + tenantName: fallbackAppointment.name, + tenantSlug: fallbackAppointment.slug, + isOwner: + metadata.primaryTenantIsOwner === + true, + jobTitle: user.jobTitle, + position: user.position, + }, + ] + : [] : [], ); } - }, [user, reset]); + }, [hanmacFamilyTenantId, tenants, user, reset]); const mutation = useMutation({ mutationFn: (data: UserUpdateRequest) => updateUser(userId, data), diff --git a/adminfront/src/features/users/UserListPage.tsx b/adminfront/src/features/users/UserListPage.tsx index 1ef5198e..5bf56a55 100644 --- a/adminfront/src/features/users/UserListPage.tsx +++ b/adminfront/src/features/users/UserListPage.tsx @@ -464,6 +464,9 @@ function UserListPage() { onChange={toggleSelectAll} /> + + {t("ui.admin.users.list.table.id", "ID")} + {t( "ui.admin.users.list.table.name_email", @@ -494,7 +497,7 @@ function UserListPage() { {query.isLoading && ( {t("msg.common.loading", "로딩 중...")} @@ -504,7 +507,7 @@ function UserListPage() { {!query.isLoading && items.length === 0 && ( {t( @@ -538,6 +541,12 @@ function UserListPage() { } /> + + {user.id} +
diff --git a/adminfront/src/features/users/orgChartPicker.test.ts b/adminfront/src/features/users/orgChartPicker.test.ts index a9707299..6ea364f7 100644 --- a/adminfront/src/features/users/orgChartPicker.test.ts +++ b/adminfront/src/features/users/orgChartPicker.test.ts @@ -1,13 +1,15 @@ import { describe, expect, it } from "vitest"; import { buildAuthenticatedOrgChartTenantPickerUrl, + buildAuthenticatedOrgChartUrl, buildOrgChartTenantPickerUrl, filterNonHanmacFamilyTenants, + isHanmacFamilyUser, parseOrgChartTenantSelection, } from "./orgChartPicker"; describe("orgChartPicker", () => { - it("builds the tenant picker embed URL from VITE_ORGCHART_URL", () => { + it("builds the tenant picker embed URL from ORGFRONT_URL", () => { expect(buildOrgChartTenantPickerUrl("https://orgchart.example.com/")).toBe( "https://orgchart.example.com/embed/picker?mode=single&select=tenant&width=400&height=600", ); @@ -36,6 +38,12 @@ describe("orgChartPicker", () => { ); }); + it("builds the chart navigation URL through the org-chart auto login entry", () => { + expect(buildAuthenticatedOrgChartUrl("https://orgchart.example.com/")).toBe( + "https://orgchart.example.com/login?auto=1&returnTo=%2Fchart", + ); + }); + it("parses the first tenant id and name from orgfront confirm messages", () => { expect( parseOrgChartTenantSelection({ @@ -115,4 +123,50 @@ describe("orgChartPicker", () => { expect(visibleTenants.map((tenant) => tenant.slug)).toEqual(["external"]); }); + + it("detects existing users as Hanmac family from tenant subtree without metadata flag", () => { + const tenants = [ + { + id: "external-id", + slug: "external", + name: "External", + type: "COMPANY", + parentId: undefined, + }, + { + id: "hanmac-family-id", + slug: "hanmac-family", + name: "한맥가족", + type: "COMPANY_GROUP", + parentId: undefined, + }, + { + id: "hanmac-company-id", + slug: "hanmac-company", + name: "한맥기술", + type: "COMPANY", + parentId: "hanmac-family-id", + }, + { + id: "hanmac-team-id", + slug: "hanmac-team", + name: "기술기획", + type: "USER_GROUP", + parentId: "hanmac-company-id", + }, + ]; + + expect( + isHanmacFamilyUser( + { + companyCode: "external", + tenant: tenants[0], + joinedTenants: [tenants[3]], + metadata: {}, + }, + tenants, + "hanmac-family-id", + ), + ).toBe(true); + }); }); diff --git a/adminfront/src/features/users/orgChartPicker.ts b/adminfront/src/features/users/orgChartPicker.ts index 17ef7517..080a72cf 100644 --- a/adminfront/src/features/users/orgChartPicker.ts +++ b/adminfront/src/features/users/orgChartPicker.ts @@ -8,6 +8,15 @@ export type TenantFilterTarget = { slug?: string; type?: string; parentId?: string | null; + name?: string; +}; + +export type HanmacFamilyUserTarget = { + companyCode?: string; + tenantSlug?: string; + tenant?: TenantFilterTarget; + joinedTenants?: TenantFilterTarget[]; + metadata?: Record; }; type OrgChartPickerMessage = { @@ -25,6 +34,10 @@ type OrgChartTenantPickerOptions = { tenantId?: string; }; +type OrgChartLoginOptions = { + returnTo?: string; +}; + function isSystemTenant(tenant: TenantFilterTarget) { const slug = tenant.slug?.trim().toLowerCase(); const type = tenant.type?.trim().toUpperCase(); @@ -66,11 +79,77 @@ function isInTenantSubtree( return false; } +function resolveHanmacFamilyTenantId( + tenants: T[], + hanmacFamilyTenantId?: string, +) { + const envTenantId = hanmacFamilyTenantId?.trim(); + if (envTenantId) return envTenantId; + + return ( + tenants.find((tenant) => tenant.slug?.toLowerCase() === "hanmac-family") + ?.id ?? "" + ); +} + +export function isHanmacFamilyTenant( + tenant: T | undefined, + tenants: T[], + hanmacFamilyTenantId?: string, +) { + if (!tenant || !tenant.id) return false; + + const rootTenantId = resolveHanmacFamilyTenantId( + tenants, + hanmacFamilyTenantId, + ); + if (!rootTenantId) return false; + + const tenantById = new Map( + tenants + .filter((item) => item.id?.trim()) + .map((item) => [item.id as string, item]), + ); + const target = tenantById.get(tenant.id) ?? tenant; + + return isInTenantSubtree(target, rootTenantId, tenantById); +} + +export function isHanmacFamilyUser( + user: HanmacFamilyUserTarget, + tenants: T[], + hanmacFamilyTenantId?: string, +) { + const metadata = user.metadata ?? {}; + if (metadata.hanmacFamily === true || metadata.userType === "hanmac") { + return true; + } + + const tenantBySlug = new Map( + tenants + .filter((tenant) => tenant.slug?.trim()) + .map((tenant) => [tenant.slug?.toLowerCase() as string, tenant]), + ); + const tenantCandidates = [ + user.tenant, + ...(user.joinedTenants ?? []), + tenantBySlug.get(user.companyCode?.toLowerCase() ?? ""), + tenantBySlug.get(user.tenantSlug?.toLowerCase() ?? ""), + ]; + + return tenantCandidates.some((tenant) => + isHanmacFamilyTenant(tenant, tenants, hanmacFamilyTenantId), + ); +} + export function filterNonHanmacFamilyTenants( tenants: T[], hanmacFamilyTenantId?: string, ) { - const rootTenantId = hanmacFamilyTenantId?.trim() ?? ""; + const rootTenantId = resolveHanmacFamilyTenantId( + tenants, + hanmacFamilyTenantId, + ); const tenantById = new Map( tenants .filter((tenant) => tenant.id?.trim()) @@ -107,11 +186,19 @@ export function buildAuthenticatedOrgChartTenantPickerUrl( baseUrl?: string, options: OrgChartTenantPickerOptions = {}, ) { - const normalizedBase = (baseUrl ?? "").replace(/\/+$/, ""); const pickerUrl = buildOrgChartTenantPickerUrl("", options); + return buildAuthenticatedOrgChartUrl(baseUrl, { returnTo: pickerUrl }); +} + +export function buildAuthenticatedOrgChartUrl( + baseUrl?: string, + options: OrgChartLoginOptions = {}, +) { + const normalizedBase = (baseUrl ?? "").replace(/\/+$/, ""); + const returnTo = options.returnTo?.trim() || "/chart"; const params = new URLSearchParams({ auto: "1", - returnTo: pickerUrl, + returnTo, }); return `${normalizedBase}/login?${params.toString()}`; diff --git a/adminfront/tests/auth.spec.ts b/adminfront/tests/auth.spec.ts index 87e5623b..5b7a9a65 100644 --- a/adminfront/tests/auth.spec.ts +++ b/adminfront/tests/auth.spec.ts @@ -100,6 +100,16 @@ test.describe("Authentication", () => { ); }); + test("should link org chart navigation through the auto login entry", async ({ + page, + }) => { + await page.goto("/"); + await expect(page.getByRole("link", { name: "조직도" })).toHaveAttribute( + "href", + "http://localhost:5175/login?auto=1&returnTo=%2Fchart", + ); + }); + test("should logout and redirect to login page", async ({ page }) => { await page.goto("/"); page.on("dialog", (dialog) => dialog.accept()); diff --git a/adminfront/tests/tenants.spec.ts b/adminfront/tests/tenants.spec.ts index 030ba89f..1e146057 100644 --- a/adminfront/tests/tenants.spec.ts +++ b/adminfront/tests/tenants.spec.ts @@ -57,13 +57,15 @@ test.describe("Tenants Management", () => { }); test("should list tenants", async ({ page }) => { + const internalTenantId = "c5839444-2de0-4a37-99b0-4f94d3de8bea"; + await page.route("**/api/v1/admin/tenants**", async (route) => { if (route.request().method() === "GET") { await route.fulfill({ json: { items: [ { - id: "1", + id: internalTenantId, name: "Tenant A", slug: "tenant-a", status: "active", @@ -90,6 +92,7 @@ test.describe("Tenants Management", () => { await expect(page.locator("table")).toContainText("Tenant A", { timeout: 10000, }); + await expect(page.locator("table")).toContainText(internalTenantId); }); test("should create a new tenant", async ({ page }) => { diff --git a/adminfront/tests/users.spec.ts b/adminfront/tests/users.spec.ts index f79f77d4..42db4a58 100644 --- a/adminfront/tests/users.spec.ts +++ b/adminfront/tests/users.spec.ts @@ -465,6 +465,40 @@ test.describe("User Management", () => { .toMatchObject({ status: "inactive" }); }); + test("should expose internal user uuid in the users table", async ({ + page, + }) => { + const internalUserId = "4d20c735-05d0-42d4-9479-0e9be74fd987"; + + await page.route(/\/admin\/users(\?.*)?$/, async (route) => { + if (route.request().method() !== "GET") { + return route.fallback(); + } + return route.fulfill({ + json: { + items: [ + { + id: internalUserId, + name: "UUID User", + email: "uuid-user@test.com", + phone: "010-2222-3333", + loginId: "uuid_login_id", + role: "user", + status: "active", + createdAt: "2026-04-01T00:00:00Z", + }, + ], + total: 1, + limit: 50, + offset: 0, + }, + }); + }); + + await page.goto("/users"); + await expect(page.locator("table")).toContainText(internalUserId); + }); + test("should create a Hanmac family user with tenant appointments and no representative affiliation", async ({ page, }) => { diff --git a/adminfront/vite.config.ts b/adminfront/vite.config.ts index 345eba0b..62c9aa1b 100644 --- a/adminfront/vite.config.ts +++ b/adminfront/vite.config.ts @@ -6,7 +6,7 @@ const buildOutDir = export default defineConfig({ plugins: [react()], - envPrefix: ["VITE_", "USERFRONT_"], + envPrefix: ["VITE_", "USERFRONT_", "ORGFRONT_"], build: { outDir: buildOutDir, }, diff --git a/deploy/templates/adminfront/vite.config.ts b/deploy/templates/adminfront/vite.config.ts index 21277ec0..6a9e944d 100644 --- a/deploy/templates/adminfront/vite.config.ts +++ b/deploy/templates/adminfront/vite.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "vite"; export default defineConfig({ plugins: [react()], - envPrefix: ["VITE_", "USERFRONT_"], + envPrefix: ["VITE_", "USERFRONT_", "ORGFRONT_"], server: { host: "127.0.0.1", // 인스턴스별 도메인을 자동으로 허용 diff --git a/deploy/templates/docker-compose.yaml b/deploy/templates/docker-compose.yaml index c6d8ed3d..b0e4233d 100644 --- a/deploy/templates/docker-compose.yaml +++ b/deploy/templates/docker-compose.yaml @@ -125,6 +125,20 @@ services: command: npm run dev -- --host 0.0.0.0 networks: [app_net] + orgfront: + image: node:20-alpine + container_name: ${COMPOSE_PROJECT_NAME}_orgfront + working_dir: /app + env_file: .env + ports: + - "${ORGFRONT_PORT}:5175" + volumes: + - ../../orgfront:/app + - ./orgfront/vite.config.ts:/app/vite.config.ts:ro + - ./orgfront/auth.ts:/app/src/lib/auth.ts:ro + command: npm run dev -- --host 0.0.0.0 --port 5175 + networks: [app_net] + networks: app_net: name: ${COMPOSE_PROJECT_NAME}_net diff --git a/deploy/templates/orgfront/auth.ts b/deploy/templates/orgfront/auth.ts new file mode 100644 index 00000000..0aa8d6a0 --- /dev/null +++ b/deploy/templates/orgfront/auth.ts @@ -0,0 +1,23 @@ +import { UserManager, WebStorageStateStore } from "oidc-client-ts"; +import type { AuthProviderProps } from "react-oidc-context"; + +export const oidcConfig: AuthProviderProps = { + authority: + import.meta.env.VITE_OIDC_AUTHORITY || + `${window.location.protocol}//${window.location.hostname}:{{USERFRONT_PORT}}/oidc`, + client_id: import.meta.env.VITE_OIDC_CLIENT_ID || "orgfront", + redirect_uri: `${window.location.origin}/auth/callback`, + response_type: "code", + scope: "openid offline_access profile email", + post_logout_redirect_uri: window.location.origin, + popup_redirect_uri: `${window.location.origin}/auth/callback`, + userStore: new WebStorageStateStore({ store: window.localStorage }), + automaticSilentRenew: false, +}; + +export const userManager = new UserManager({ + ...oidcConfig, + authority: oidcConfig.authority || "", + client_id: oidcConfig.client_id || "", + redirect_uri: oidcConfig.redirect_uri || "", +}); diff --git a/deploy/templates/orgfront/vite.config.ts b/deploy/templates/orgfront/vite.config.ts new file mode 100644 index 00000000..8e20e087 --- /dev/null +++ b/deploy/templates/orgfront/vite.config.ts @@ -0,0 +1,36 @@ +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +const allowedHosts = [ + "{{ORGFRONT_DOMAIN}}", + "baron-orgchart.hmac.kr", + "localhost", + "127.0.0.1", +].filter(Boolean); + +export default defineConfig({ + plugins: [react()], + envPrefix: ["VITE_", "USERFRONT_"], + server: { + host: "127.0.0.1", + // 인스턴스별 도메인을 자동으로 허용합니다. + allowedHosts, + proxy: { + "/api": { + target: process.env.API_PROXY_TARGET || "http://backend:{{BACKEND_PORT}}", + changeOrigin: true, + }, + }, + }, + preview: { + host: "127.0.0.1", + port: 5175, + allowedHosts, + proxy: { + "/api": { + target: process.env.API_PROXY_TARGET || "http://backend:{{BACKEND_PORT}}", + changeOrigin: true, + }, + }, + }, +}); diff --git a/docker-compose.yaml b/docker-compose.yaml index e9c823aa..d5243e6d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -86,7 +86,7 @@ services: orgfront: build: - context: ../baron-orgchart + context: ./orgfront dockerfile: Dockerfile container_name: baron_orgfront env_file: @@ -98,7 +98,7 @@ services: ports: - "${ORGFRONT_PORT:-5175}:5175" volumes: - - ../baron-orgchart:/app + - ./orgfront:/app - ./locales:/locales - /app/node_modules networks: diff --git a/docker/docker-compose.staging.template.yaml b/docker/docker-compose.staging.template.yaml index b1d0558d..926ff967 100644 --- a/docker/docker-compose.staging.template.yaml +++ b/docker/docker-compose.staging.template.yaml @@ -57,6 +57,20 @@ services: networks: - baron_net + orgfront: + image: ${ORGFRONT_IMAGE_NAME}:${IMAGE_TAG} + container_name: baron_orgfront + restart: unless-stopped + env_file: + - .env + environment: + - APP_ENV=stage + - API_PROXY_TARGET=http://baron_backend:3000 + ports: + - "${ORGFRONT_PORT:-5175}:5175" + networks: + - baron_net + userfront: image: ${USERFRONT_IMAGE_NAME}:${IMAGE_TAG} container_name: baron_userfront diff --git a/docker/staging_pull_compose.template.yaml b/docker/staging_pull_compose.template.yaml index e65cb04d..b1bdd7b3 100644 --- a/docker/staging_pull_compose.template.yaml +++ b/docker/staging_pull_compose.template.yaml @@ -414,6 +414,25 @@ services: networks: - baron_net + orgfront: + build: + context: ./orgfront + dockerfile: Dockerfile + container_name: baron_orgfront + env_file: + - .env + environment: + - APP_ENV=${APP_ENV:-development} + - API_PROXY_TARGET=http://baron_backend:3000 + - USERFRONT_URL=${USERFRONT_URL} + ports: + - "${ORGFRONT_PORT:-5175}:5175" + volumes: + - ./orgfront:/app + - /app/node_modules + networks: + - baron_net + userfront: build: context: . diff --git a/docs/trouble-shooting/issue-663-hanmac-family-orgfront-sync.md b/docs/trouble-shooting/issue-663-hanmac-family-orgfront-sync.md new file mode 100644 index 00000000..7e35d10f --- /dev/null +++ b/docs/trouble-shooting/issue-663-hanmac-family-orgfront-sync.md @@ -0,0 +1,43 @@ +# Issue #663: 한맥가족 사용자 테넌트 동기화 및 직무/직급 표시 정리 + +## 개요 + +adminfront 사용자 관리 화면과 orgfront 조직도/조직 선택기 사이의 한맥가족 사용자 표시 기준을 정리했다. + +기존에는 사용자 상세 화면에서 `metadata.hanmacFamily` 값에 의존해 한맥가족 탭을 결정했다. 이 방식은 기존 사용자가 이미 한맥가족 테넌트 트리에 소속되어 있어도 metadata 플래그가 없으면 외부 기업 회원으로 분류될 수 있었다. + +## 정책 기준 + +- `docs/organization-chart-policy.md`에 따라 한맥가족 조직은 `COMPANY_GROUP` 아래의 `COMPANY`/`USER_GROUP` 테넌트 계층으로 판단한다. +- 한맥가족 사용자의 직무/직급은 단일 사용자 필드보다 소속별 `additionalAppointments`를 우선한다. +- orgfront 표시명은 직무가 있으면 `이름(직무) 직급` 형태를 사용한다. + +## 구현 요약 + +- adminfront + - 한맥가족 root tenant 및 subtree 판정 유틸을 추가했다. + - 기존 사용자의 `tenant`, `joinedTenants`, `companyCode`, `tenantSlug`를 기준으로 한맥가족 여부를 계산한다. + - 사용자 상세 초기화 시 한맥가족 subtree 소속이면 `metadata.hanmacFamily`가 없어도 한맥가족 탭을 표시한다. + - 한맥가족 저장 시 기존 단일 `position`/`jobTitle` payload를 비우고 `additionalAppointments`를 사용한다. +- orgfront + - 사용자 표시명 공통 유틸을 추가했다. + - 조직도와 조직 선택기 모두 동일한 표시명 로직을 사용한다. + - `metadata.additionalAppointments`에 현재 테넌트와 매칭되는 직무/직급이 있으면 이를 우선 사용한다. + +## 검증 + +- RED 확인 + - `adminfront`: 한맥가족 subtree 사용자 판정 유틸 부재로 unit test 실패 확인. + - `orgfront`: 조직도/조직 선택기에서 `이름(직무) 직급` 표시가 없어 Playwright test 실패 확인. +- GREEN 확인 + - `adminfront`: `npm run test:unit -- src/features/users/orgChartPicker.test.ts` + - `adminfront`: `npm run test:unit` + - `adminfront`: `npm run build` + - `orgfront`: `npm run lint` + - `orgfront`: `npm run build` + - `orgfront`: `npx playwright test tests/orgchart-picker.spec.ts tests/orgchart-vector-render.spec.ts --project=chromium` + +## 남은 정책/운영 메모 + +- 이슈는 `한맥가족사 조직도 반영` 마일스톤에 연결했다. +- 해당 마일스톤은 Due Date가 비어 있다. 목표 Due Date를 정해 마일스톤에 반영하는 것이 좋다. diff --git a/orgfront b/orgfront deleted file mode 160000 index c9878bf8..00000000 --- a/orgfront +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c9878bf8d0ca0d7a2b94afd89b209060b14147d4 diff --git a/orgfront/.dockerignore b/orgfront/.dockerignore new file mode 100644 index 00000000..6220de51 --- /dev/null +++ b/orgfront/.dockerignore @@ -0,0 +1,8 @@ +node_modules +dist +.git +.gitignore +*.md +docker-compose.yml +.env* +npm-debug.log diff --git a/orgfront/.gitignore b/orgfront/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/orgfront/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/orgfront/Dockerfile b/orgfront/Dockerfile new file mode 100644 index 00000000..426db35e --- /dev/null +++ b/orgfront/Dockerfile @@ -0,0 +1,20 @@ +FROM node:lts + +WORKDIR /app + +# 패키지 정보 복사 및 의존성 설치 +COPY package*.json ./ +RUN npm ci + +# 프로덕션 서빙을 위한 serve 패키지 글로벌 설치 +RUN npm install -g serve + +# 소스 코드 복사 +COPY . . + +# Vite 기본 포트 +EXPOSE 5175 + +# 실행 스크립트: APP_ENV에 따라 개발 서버 또는 빌드 후 서빙 +RUN chmod +x ./scripts/runtime-mode.sh +CMD ["sh", "./scripts/runtime-mode.sh"] diff --git a/orgfront/Dockerfile2 b/orgfront/Dockerfile2 new file mode 100644 index 00000000..5f650a84 --- /dev/null +++ b/orgfront/Dockerfile2 @@ -0,0 +1,20 @@ +FROM node:lts + +WORKDIR /app + +# 패키지 정보 복사 및 의존성 설치 +COPY package*.json ./ +RUN npm ci + +# 프로덕션 서빙을 위한 serve 패키지 글로벌 설치 +RUN npm install -g serve + +# 소스 코드 복사 +COPY . . + +# Vite 기본 포트 +EXPOSE 5173 + +# 실행 스크립트: APP_ENV에 따라 개발 서버 또는 빌드 후 서빙 +RUN chmod +x ./scripts/runtime-mode.sh +CMD ["sh", "./scripts/runtime-mode.sh"] diff --git a/orgfront/README.md b/orgfront/README.md new file mode 100644 index 00000000..14bf7786 --- /dev/null +++ b/orgfront/README.md @@ -0,0 +1,106 @@ +# Baron OrgChart (바론 조직도 서비스) + +Baron SSO 시스템과 연동되는 독립적인 **조직도 시각화(Organization Chart) 웹 애플리케이션**입니다. 기존 관리자 시스템(Adminfront)에 종속되어 있던 기능을 별도의 서비스(RP: Relying Party)로 분리하여 구축했습니다. + +## 🌟 주요 기능 + +* **독립된 SSO 인증 (Ory Hydra):** Baron SSO(Ory 기반)를 통한 안전한 OAuth2/OIDC 로그인 지원 +* **딥링크(Deep Link) 지원:** 특정 부서의 조직도로 바로 접근 가능한 공유 링크 (`/chart/:tenantId` 또는 `/chart/:slug`) 지원 +* **고급 트리뷰 디자인:** 밝은 테마 기반의 직관적인 계층형 트리뷰 제공 +* **Keto ReBAC 권한 연동 (예정):** 사용자의 소속 부서 및 권한 레벨에 따라 열람 가능한 조직도가 동적으로 제어됩니다. + +## 🛠️ 기술 스택 + +* **프레임워크:** React 19 + Vite + TypeScript +* **스타일링:** Tailwind CSS +* **인증 연동:** `react-oidc-context`, `oidc-client-ts` +* **상태 관리:** React Query (`@tanstack/react-query`) +* **아이콘:** Lucide React + +## 🧩 조직도 데이터 연동 구조 + +조직도 화면은 Baron Admin에서 관리한 **테넌트(Tenant)**와 **사용자(User)** 정보를 Backend API로 받아와 프론트에서 트리 구조로 조립합니다. Adminfront 화면에 직접 의존하지 않고, 동일한 Admin API 데이터를 사용하는 독립 RP로 동작합니다. + +### 인증 및 API 호출 + +* 모든 인증 화면은 `react-oidc-context`를 통해 OIDC Access Token을 얻고, API 요청 시 `Authorization: Bearer ` 헤더를 붙입니다. +* 사용자가 작업 테넌트를 선택한 경우 `localStorage.dev_tenant_id` 값을 `X-Tenant-ID` 헤더로 함께 전달합니다. +* API Base URL은 `VITE_DEV_API_BASE`, `VITE_ADMIN_API_BASE`, `/api` 순서로 결정됩니다. + +### 일반 조직도 화면 + +`/chart`와 `/chart/:tenantId`는 로그인된 사용자를 대상으로 다음 API를 호출합니다. + +| 용도 | API | 주요 사용 필드 | +| --- | --- | --- | +| 테넌트 목록 | `GET /v1/admin/tenants?limit=10000&offset=0` | `id`, `type`, `name`, `slug`, `parentId`, `memberCount`, `status` | +| 사용자 목록 | `GET /v1/admin/users?limit=5000&offset=0` | `id`, `email`, `name`, `status`, `tenantSlug`, `companyCode`, `joinedTenants`, `position`, `jobTitle` | + +테넌트는 Baron Admin에서 입력한 `parentId` 관계를 기준으로 트리로 변환합니다. 현재 루트 후보는 `type === "COMPANY_GROUP"`인 테넌트를 우선 사용하고, 없으면 최상위 테넌트를 사용합니다. 회사 필터는 루트 하위의 `type === "COMPANY"` 테넌트로 구성됩니다. + +사용자는 다음 순서로 조직도 노드에 매핑됩니다. + +1. `status === "active"`인 사용자만 사용합니다. +2. `@hanmac.kr` 이메일은 현재 조직도 표시 대상에서 제외합니다. +3. 사용자의 `companyCode`가 있으면 해당 값을 테넌트 `slug`와 매칭합니다. +4. `companyCode`가 없으면 `tenantSlug`를 사용합니다. +5. `joinedTenants`가 있으면 각 joined tenant의 `slug`에도 같은 사용자를 추가합니다. +6. 같은 테넌트 노드 안에서 동일 사용자 `id`는 중복 추가하지 않습니다. + +각 조직도 노드는 테넌트명(`name`)을 헤더로 사용하고, 해당 테넌트 `slug`에 매핑된 사용자를 구성원 목록으로 표시합니다. 구성원은 `position`/`jobTitle` 기준으로 정렬되며, 표시 직무는 `jobTitle || position || "사원"` 순서로 결정됩니다. + +### 공유 조직도 화면 + +`/chart?token=` 형태의 공유 링크는 인증 체크를 건너뛰고 다음 공개 API만 호출합니다. + +| 용도 | API | 주요 사용 필드 | +| --- | --- | --- | +| 공유 조직도 | `GET /v1/public/orgchart?token=` | `tenants`, `users`, `sharedWith` | + +공개 API 응답의 `tenants`와 `users`는 일반 조직도와 같은 방식으로 트리 및 구성원 목록에 매핑됩니다. 단, 공개 응답은 서버가 공유 범위에 맞게 이미 필터링한 데이터로 간주합니다. + +### 조직 선택기 + +`/picker`와 `/embed/picker`도 Baron Admin의 테넌트/사용자 데이터를 사용합니다. + +* `GET /v1/admin/tenants` +* `GET /v1/admin/users` + +선택기는 테넌트 노드와 사용자 노드를 함께 보여주며, 사용자는 `companyCode || tenantSlug`와 테넌트 `slug`를 기준으로 배치합니다. 임베딩 모드에서는 선택 결과를 `postMessage`로 부모 화면에 전달합니다. + +## 🚀 로컬 환경 실행 가이드 + +### 1. 저장소 복제 및 의존성 설치 +```bash +git clone https://gitea.hmac.kr/baron/baron-orgchart.git +cd baron-orgchart +npm install +``` + +### 2. 환경 변수 설정 +프로젝트 루트에 `.env.local` 파일을 생성하고 아래 환경 변수를 설정합니다. (개발 환경 기준) + +```env +# Baron SSO(Gateway)의 OIDC 엔드포인트 +VITE_OIDC_AUTHORITY=http://localhost:5000/oidc + +# Hydra에 등록된 Client ID +VITE_OIDC_CLIENT_ID=orgfront + +# Backend API 주소 +VITE_ADMIN_API_BASE=http://localhost:5000/api +``` + +### 3. 개발 서버 실행 +```bash +npm run dev +``` +기본적으로 `http://localhost:5175` 에서 실행됩니다. + +## 🔗 관련 문서 및 이슈 +* [조직도 임베딩 토큰 설계](docs/orgchart-embedding-token-design.md) +* [Issue #544: 조직도 탭 분리 및 스타일 변경](https://gitea.hmac.kr/baron/baron-sso/issues/544) +* [Issue #545: 조직도 권한 설정](https://gitea.hmac.kr/baron/baron-sso/issues/545) + +--- +*Powered by Baron SSO* diff --git a/orgfront/biome.json b/orgfront/biome.json new file mode 100644 index 00000000..44d528eb --- /dev/null +++ b/orgfront/biome.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "formatter": { + "indentStyle": "space" + }, + "linter": { + "enabled": true, + "rules": { + "style": { + "useEnumInitializers": "off" + }, + "a11y": { + "noLabelWithoutControl": "off" + } + } + }, + "organizeImports": { + "enabled": true + }, + "files": { + "ignore": [ + "dist", + "node_modules", + "tsconfig*.json", + "test-results", + "playwright-report" + ] + } +} diff --git a/orgfront/docker-compose.yml b/orgfront/docker-compose.yml new file mode 100644 index 00000000..46c2abc6 --- /dev/null +++ b/orgfront/docker-compose.yml @@ -0,0 +1,19 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: baron-orgchart + ports: + - "5175:5175" + environment: + - APP_ENV=development + - VITE_OIDC_AUTHORITY=http://localhost:5000/oidc + - VITE_OIDC_CLIENT_ID=orgfront + - VITE_API_URL=http://localhost:5000/api + - API_PROXY_TARGET=http://localhost:5000 + volumes: + - .:/app + - /app/node_modules + stdin_open: true + tty: true diff --git a/orgfront/docs/orgchart-embedding-token-design.md b/orgfront/docs/orgchart-embedding-token-design.md new file mode 100644 index 00000000..ba6ce594 --- /dev/null +++ b/orgfront/docs/orgchart-embedding-token-design.md @@ -0,0 +1,125 @@ +# 조직도 임베딩 토큰 설계 + +## 목적 + +조직도 임베딩 검증 화면은 외부 서비스가 Baron OrgChart의 조직 선택기 또는 조직도 데이터를 안전하게 사용할 수 있는지를 확인하는 도구입니다. 토큰 설계는 다음 두 가지 요구를 동시에 다룹니다. + +* 최상위 테넌트 권한 범위에 맞춰 조직도 접근 토큰을 발급한다. +* 특정 서비스가 장기간 안정적으로 임베딩할 수 있도록 서비스 전용 토큰을 생성하고 갱신한다. + +## 방식 A: 최상위 테넌트 권한 기반 토큰 + +이 방식은 로그인한 사용자의 현재 권한을 기준으로 단기 공유 토큰을 발급합니다. 관리자가 `COMPANY_GROUP` 또는 최상위 테넌트 범위를 선택하면, 서버는 사용자가 해당 범위를 볼 수 있는지 확인한 뒤 제한된 수명의 토큰을 반환합니다. + +### 권장 API + +```http +POST /v1/orgchart/embed-tokens +Authorization: Bearer +X-Tenant-ID: +Content-Type: application/json +``` + +```json +{ + "scopeType": "tenant_root", + "rootTenantId": "group-hmac", + "allowedModes": ["single", "multiple"], + "allowedSelectableTypes": ["tenant", "user", "both"], + "expiresInSeconds": 3600 +} +``` + +### 서버 검증 + +* 요청자는 `rootTenantId` 또는 그 상위 범위를 관리할 수 있어야 합니다. +* 토큰에는 `rootTenantId`, 허용 선택 모드, 허용 선택 대상, 만료 시각을 포함합니다. +* 토큰은 서버 저장형 opaque token을 우선 권장합니다. 즉시 폐기와 감사 로그 추적이 쉽기 때문입니다. +* JWT를 사용할 경우 `jti`를 저장해 폐기 목록을 운영해야 합니다. + +### 장점 + +* 현재 관리자 권한 모델과 자연스럽게 연결됩니다. +* 단기 검증, 데모, 운영자 테스트에 적합합니다. +* 토큰 범위가 명확해 권한 사고 범위를 줄일 수 있습니다. + +### 한계 + +* 서비스가 장기간 임베딩하려면 사용자가 주기적으로 재발급해야 합니다. +* 외부 서비스 단위의 소유자, 회전 주기, 폐기 정책을 별도로 관리하기 어렵습니다. + +## 방식 B: 특정 서비스용 토큰 생성 및 갱신 + +이 방식은 Baron Admin 또는 OrgFront 관리 화면에서 외부 서비스별 임베딩 클라이언트를 등록하고, 각 서비스에 장기 토큰 또는 회전 가능한 토큰 세트를 발급합니다. + +### 권장 모델 + +```json +{ + "id": "embed-client-001", + "serviceName": "crm-dashboard", + "ownerTenantId": "group-hmac", + "rootTenantId": "company-baron", + "allowedOrigins": ["https://crm.example.com"], + "allowedModes": ["single", "multiple"], + "allowedSelectableTypes": ["user"], + "status": "active", + "expiresAt": "2026-07-31T00:00:00.000Z", + "lastRotatedAt": "2026-04-29T00:00:00.000Z" +} +``` + +### 권장 API + +```http +POST /v1/admin/orgchart/embed-clients +GET /v1/admin/orgchart/embed-clients +POST /v1/admin/orgchart/embed-clients/{clientId}/rotate-token +POST /v1/admin/orgchart/embed-clients/{clientId}/revoke +GET /v1/public/orgchart/embed-config?token= +``` + +### 서버 검증 + +* 서비스 토큰은 특정 `rootTenantId` 이하 데이터에만 접근할 수 있어야 합니다. +* `allowedOrigins`가 설정된 경우 `Origin` 헤더를 검증합니다. +* 토큰 회전 시 기존 토큰과 신규 토큰이 함께 유효한 grace period를 짧게 둘 수 있습니다. +* 모든 생성, 갱신, 폐기, 사용 이벤트는 감사 로그에 남깁니다. +* 토큰 원문은 최초 생성 또는 회전 직후에만 보여주고, 서버에는 해시만 저장합니다. + +### 장점 + +* 외부 서비스별 접근 범위, 만료, 회전, 폐기 정책을 독립적으로 관리할 수 있습니다. +* 운영 환경의 장기 임베딩에 적합합니다. +* 감사 로그와 장애 대응이 명확합니다. + +### 한계 + +* 별도 관리 화면과 백엔드 저장 모델이 필요합니다. +* 토큰 회전 정책, Origin 검증, 만료 알림 등 운영 기능의 설계 범위가 커집니다. + +## 권장 적용 순서 + +1. 먼저 방식 A를 구현해 임베딩 검증 화면에서 최상위 테넌트 권한 기반 단기 토큰을 발급합니다. +2. 토큰 payload와 감사 로그 이벤트 스키마를 방식 B에서도 재사용할 수 있게 고정합니다. +3. 외부 서비스 운영 요구가 확정되면 방식 B의 embed client 관리 API와 화면을 추가합니다. +4. 방식 B 도입 후에도 방식 A는 관리자 테스트용 단기 토큰 발급 기능으로 유지합니다. + +## 프론트엔드 반영 방향 + +임베딩 검증 화면은 현재 선택 모드와 선택 대상 조합을 바꿔 iframe을 갱신합니다. 토큰 기능이 추가되면 다음 값을 함께 표시해야 합니다. + +* 발급 주체: 현재 사용자 또는 서비스 클라이언트 +* 접근 루트: `rootTenantId` +* 허용 Origin +* 허용 선택 모드: `single`, `multiple` +* 허용 선택 대상: `tenant`, `user`, `both` +* 만료 시각과 갱신 가능 여부 + +iframe URL은 다음 형태를 기준으로 확장합니다. + +```text +/embed/picker?token=&mode=multiple&select=both +``` + +서버는 토큰의 허용 범위와 URL query가 충돌할 경우 더 좁은 범위를 적용하거나 요청을 거절해야 합니다. diff --git a/orgfront/hydra-rp-dummy.py b/orgfront/hydra-rp-dummy.py new file mode 100644 index 00000000..101124b0 --- /dev/null +++ b/orgfront/hydra-rp-dummy.py @@ -0,0 +1,188 @@ +from http.server import BaseHTTPRequestHandler, HTTPServer +from http import cookiejar +import json +import os +import threading +import urllib.parse +import urllib.request + +CLIENT_ID = os.environ["CLIENT_ID"] +SUBJECT = os.environ["SUBJECT"] +REDIRECT_URI = os.environ["REDIRECT_URI"] +SCOPE = os.environ["SCOPE"] +STATE = os.environ["STATE"] +NONCE = os.environ["NONCE"] +ADMIN_BASE = os.environ.get("HYDRA_ADMIN_URL", "http://127.0.0.1:4445") +PUBLIC_BASE = os.environ.get("HYDRA_PUBLIC_URL", "http://127.0.0.1:4444") + + +def _put_json(url: str, payload: dict) -> dict: + data = json.dumps(payload).encode("utf-8") + req = urllib.request.Request(url, data=data, method="PUT") + req.add_header("Content-Type", "application/json") + with urllib.request.urlopen(req, timeout=5) as resp: + body = resp.read().decode("utf-8") + return json.loads(body) if body else {} + + +def accept_login(challenge: str) -> str: + url = f"{ADMIN_BASE}/oauth2/auth/requests/login/accept?login_challenge={urllib.parse.quote(challenge)}" + payload = {"subject": SUBJECT, "remember": True, "remember_for": 3600} + data = _put_json(url, payload) + return data.get("redirect_to", "") + + +def accept_consent(challenge: str) -> str: + url = f"{ADMIN_BASE}/oauth2/auth/requests/consent/accept?consent_challenge={urllib.parse.quote(challenge)}" + payload = {"grant_scope": ["openid", "profile", "email"], "remember": True, "remember_for": 3600} + data = _put_json(url, payload) + return data.get("redirect_to", "") + + +def _location_from_response(url: str, cookie_header: str | None) -> str: + req = urllib.request.Request(url, method="GET") + if cookie_header: + req.add_header("Cookie", cookie_header) + opener = urllib.request.build_opener(NoRedirect()) + try: + opener.open(req, timeout=5) + except urllib.error.HTTPError as err: + return err.headers.get("Location", "") + return "" + + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + raise urllib.error.HTTPError(newurl, code, msg, headers, fp) + + +class Handler(BaseHTTPRequestHandler): + def do_GET(self): + parsed = urllib.parse.urlparse(self.path) + params = urllib.parse.parse_qs(parsed.query) + login_challenge = (params.get("login_challenge") or [""])[0] + consent_challenge = (params.get("consent_challenge") or [""])[0] + login_verifier = (params.get("login_verifier") or [""])[0] + consent_verifier = (params.get("consent_verifier") or [""])[0] + + if parsed.path == "/oauth2/auth" and consent_verifier: + query = urllib.parse.urlencode({ + "consent_verifier": consent_verifier, + "client_id": (params.get("client_id") or [""])[0], + "redirect_uri": (params.get("redirect_uri") or [""])[0], + "response_type": (params.get("response_type") or [""])[0], + "scope": (params.get("scope") or [""])[0], + "state": (params.get("state") or [""])[0], + "nonce": (params.get("nonce") or [""])[0], + }) + public_url = f"{PUBLIC_BASE}/oauth2/auth?{query}" + location = _location_from_response(public_url, self.headers.get("Cookie")) + print(f"consent_verifier_location={location}") + if not location: + self.send_response(400) + self.end_headers() + self.wfile.write(b"missing redirect location") + return + self.send_response(302) + self.send_header("Location", location) + self.end_headers() + return + + if parsed.path == "/oauth2/auth" and login_verifier: + query = urllib.parse.urlencode({ + "login_verifier": login_verifier, + "client_id": (params.get("client_id") or [""])[0], + "redirect_uri": (params.get("redirect_uri") or [""])[0], + "response_type": (params.get("response_type") or [""])[0], + "scope": (params.get("scope") or [""])[0], + "state": (params.get("state") or [""])[0], + "nonce": (params.get("nonce") or [""])[0], + }) + public_url = f"{PUBLIC_BASE}/oauth2/auth?{query}" + location = _location_from_response(public_url, self.headers.get("Cookie")) + print(f"login_verifier_location={location}") + if not location: + self.send_response(400) + self.end_headers() + self.wfile.write(b"missing redirect location") + return + consent_challenge = urllib.parse.parse_qs(urllib.parse.urlparse(location).query).get( + "consent_challenge", + [""], + )[0] + if not consent_challenge: + self.send_response(400) + self.end_headers() + self.wfile.write(f"missing consent_challenge location={location}".encode("utf-8")) + return + redirect_to = accept_consent(consent_challenge) + if not redirect_to: + self.send_response(500) + self.end_headers() + self.wfile.write(b"consent accept failed") + return + self.send_response(302) + self.send_header("Location", redirect_to) + self.end_headers() + return + + if login_challenge: + redirect_to = accept_login(login_challenge) + elif consent_challenge: + redirect_to = accept_consent(consent_challenge) + else: + redirect_to = "" + + if not redirect_to: + self.send_response(400) + self.end_headers() + self.wfile.write(b"missing challenge") + return + + self.send_response(302) + self.send_header("Location", redirect_to) + self.end_headers() + + def log_message(self, format, *args): + return + + +class StopAtRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + if newurl.startswith(REDIRECT_URI): + raise urllib.error.HTTPError(newurl, code, msg, headers, fp) + return super().redirect_request(req, fp, code, msg, headers, newurl) + + +def main(): + server = HTTPServer(("127.0.0.1", 3000), Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + + encoded_redirect = urllib.parse.quote(REDIRECT_URI, safe="") + encoded_scope = urllib.parse.quote(SCOPE, safe="") + auth_url = ( + f"{PUBLIC_BASE}/oauth2/auth?response_type=code" + f"&client_id={CLIENT_ID}" + f"&redirect_uri={encoded_redirect}" + f"&scope={encoded_scope}" + f"&state={STATE}" + f"&nonce={NONCE}" + ) + + jar = cookiejar.CookieJar() + opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar), StopAtRedirect()) + try: + opener.open(auth_url, timeout=10) + except urllib.error.HTTPError as err: + body = err.read().decode("utf-8") if hasattr(err, "read") else "" + print(f"error_url={err.geturl()}") + print(f"error_code={err.code}") + if body: + print(f"error_body={body}") + finally: + server.shutdown() + + +if __name__ == "__main__": + main() diff --git a/orgfront/index.html b/orgfront/index.html new file mode 100644 index 00000000..696d2bdb --- /dev/null +++ b/orgfront/index.html @@ -0,0 +1,13 @@ + + + + + + + 바론 조직도 서비스 + + +
+ + + diff --git a/orgfront/package-lock.json b/orgfront/package-lock.json new file mode 100644 index 00000000..bcd9971d --- /dev/null +++ b/orgfront/package-lock.json @@ -0,0 +1,3220 @@ +{ + "name": "orgfront", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "orgfront", + "version": "0.0.0", + "dependencies": { + "@radix-ui/react-avatar": "^1.1.4", + "@radix-ui/react-scroll-area": "^1.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-switch": "^1.1.2", + "@tanstack/react-query": "^5.66.8", + "@tanstack/react-query-devtools": "^5.66.8", + "axios": "^1.7.9", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.563.0", + "oidc-client-ts": "^3.4.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-hook-form": "^7.71.1", + "react-oidc-context": "^3.3.0", + "react-router-dom": "^6.28.2", + "tailwind-merge": "^3.4.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@playwright/test": "^1.58.0", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.4.23", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.14", + "tailwindcss-animate": "^1.0.7", + "typescript": "~5.9.3", + "vite": "^8.0.3" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@biomejs/biome": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", + "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "dev": true, + "hasInstallScript": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "1.9.4", + "@biomejs/cli-darwin-x64": "1.9.4", + "@biomejs/cli-linux-arm64": "1.9.4", + "@biomejs/cli-linux-arm64-musl": "1.9.4", + "@biomejs/cli-linux-x64": "1.9.4", + "@biomejs/cli-linux-x64-musl": "1.9.4", + "@biomejs/cli-win32-arm64": "1.9.4", + "@biomejs/cli-win32-x64": "1.9.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", + "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", + "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", + "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", + "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", + "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", + "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", + "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", + "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", + "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.93.0.tgz", + "integrity": "sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.91.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.91.3.tgz", + "integrity": "sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.93.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.90.20", + "react": "^18 || ^19" + } + }, + "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/@types/node": { + "version": "24.10.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.13.tgz", + "integrity": "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "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/autoprefixer": { + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "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==", + "devOptional": true, + "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/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "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/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "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/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "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/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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/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/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lucide-react": { + "version": "0.563.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", + "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/oidc-client-ts": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.4.1.tgz", + "integrity": "sha512-jNdst/U28Iasukx/L5MP6b274Vr7ftQs6qAhPBCvz6Wt5rPCA+Q/tUmCzfCHHWweWw5szeMy2Gfrm1rITwUKrw==", + "license": "Apache-2.0", + "dependencies": { + "jwt-decode": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "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": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-hook-form": { + "version": "7.71.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", + "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-oidc-context": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/react-oidc-context/-/react-oidc-context-3.3.0.tgz", + "integrity": "sha512-302T/ma4AOVAxrHdYctDSKXjCq9KNHT564XEO2yOPxRfxEP58xa4nz+GQinNl8x7CnEXECSM5JEjQJk3Cr5BvA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "oidc-client-ts": "^3.1.0", + "react": ">=16.14.0" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "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": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.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/vite/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/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/orgfront/package.json b/orgfront/package.json new file mode 100644 index 00000000..8ec15b1a --- /dev/null +++ b/orgfront/package.json @@ -0,0 +1,52 @@ +{ + "name": "orgfront", + "private": true, + "version": "0.0.0", + "type": "module", + "engines": { + "node": ">=24.0.0" + }, + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "tsc -b && vite build", + "lint": "biome check .", + "preview": "vite preview", + "test": "playwright test", + "test:roles": "playwright test tests/devfront-role-switch-report.spec.ts", + "test:ui": "playwright test --ui" + }, + "dependencies": { + "@radix-ui/react-avatar": "^1.1.4", + "@radix-ui/react-scroll-area": "^1.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-switch": "^1.1.2", + "@tanstack/react-query": "^5.66.8", + "@tanstack/react-query-devtools": "^5.66.8", + "axios": "^1.7.9", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.563.0", + "oidc-client-ts": "^3.4.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-hook-form": "^7.71.1", + "react-oidc-context": "^3.3.0", + "react-router-dom": "^6.28.2", + "tailwind-merge": "^3.4.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@playwright/test": "^1.58.0", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.4.23", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.14", + "tailwindcss-animate": "^1.0.7", + "typescript": "~5.9.3", + "vite": "^8.0.3" + } +} diff --git a/orgfront/playwright.config.ts b/orgfront/playwright.config.ts new file mode 100644 index 00000000..e94653ff --- /dev/null +++ b/orgfront/playwright.config.ts @@ -0,0 +1,65 @@ +import { defineConfig, devices } from "@playwright/test"; + +const configuredWorkers = process.env.PLAYWRIGHT_WORKERS + ? Number.parseInt(process.env.PLAYWRIGHT_WORKERS, 10) + : undefined; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: configuredWorkers ?? (process.env.CI ? 1 : undefined), + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [["html", { open: "never" }], ["list"]], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:5175", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: process.env.CI + ? "npm run build && npm run preview -- --host 0.0.0.0 --port 5175" + : "npm run dev -- --host 0.0.0.0 --port 5175", + url: "http://localhost:5175", + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/orgfront/postcss.config.js b/orgfront/postcss.config.js new file mode 100644 index 00000000..2aa7205d --- /dev/null +++ b/orgfront/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/orgfront/public/vite.svg b/orgfront/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/orgfront/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/orgfront/scripts/runtime-mode.sh b/orgfront/scripts/runtime-mode.sh new file mode 100644 index 00000000..aa41dce1 --- /dev/null +++ b/orgfront/scripts/runtime-mode.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env sh +set -eu + +app_env="$(printf '%s' "${APP_ENV:-development}" | tr '[:upper:]' '[:lower:]')" + +case "$app_env" in + production|prod|stage|staging) + mode="production" + ;; + *) + mode="development" + ;; +esac + +if [ "${1:-}" = "--print-mode" ]; then + printf '%s\n' "$mode" + exit 0 +fi + +if [ "$mode" = "production" ]; then + echo "Running in production mode with Vite preview..." + exec sh -c "npm run build && npm run preview -- --host 0.0.0.0" +fi + +echo "Running in development mode..." +exec npm run dev -- --host 0.0.0.0 diff --git a/orgfront/src/app/queryClient.ts b/orgfront/src/app/queryClient.ts new file mode 100644 index 00000000..06c9afde --- /dev/null +++ b/orgfront/src/app/queryClient.ts @@ -0,0 +1,11 @@ +import { QueryClient } from "@tanstack/react-query"; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + refetchOnWindowFocus: false, + retry: 1, + }, + }, +}); diff --git a/orgfront/src/app/routes.tsx b/orgfront/src/app/routes.tsx new file mode 100644 index 00000000..d23f9c5a --- /dev/null +++ b/orgfront/src/app/routes.tsx @@ -0,0 +1,46 @@ +import { Navigate, createBrowserRouter } from "react-router-dom"; +import AuthCallbackPage from "../features/auth/AuthCallbackPage"; +import AuthGuard from "../features/auth/AuthGuard"; +import LoginPage from "../features/auth/LoginPage"; +import { TenantOrgChartPage } from "../features/orgchart/routes/OrgChartPage"; +import { OrgFrontLayout } from "../features/orgchart/routes/OrgFrontLayout"; +import { OrgPickerEmbedPreviewPage } from "../features/orgchart/routes/OrgPickerEmbedPreviewPage"; +import { + OrgPickerEmbedPage, + OrgPickerPage, +} from "../features/orgchart/routes/OrgPickerPage"; + +export const router = createBrowserRouter( + [ + { + path: "/login", + element: , + }, + { + path: "/auth/callback", + element: , + }, + { + path: "/", + element: , + children: [ + { index: true, element: }, + { + element: , + children: [ + { path: "chart", element: }, + { path: "chart/:tenantId", element: }, + { path: "picker", element: }, + { path: "embed-preview", element: }, + ], + }, + { path: "embed/picker", element: }, + ], + }, + ], + { + future: { + v7_startTransition: true, + }, + } as unknown as Parameters[1], +); diff --git a/orgfront/src/assets/react.svg b/orgfront/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/orgfront/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/orgfront/src/components/common/ForbiddenMessage.tsx b/orgfront/src/components/common/ForbiddenMessage.tsx new file mode 100644 index 00000000..9466ce36 --- /dev/null +++ b/orgfront/src/components/common/ForbiddenMessage.tsx @@ -0,0 +1,51 @@ +import { ShieldAlert } from "lucide-react"; +import { useAuth } from "react-oidc-context"; +import { t } from "../../lib/i18n"; +import { resolveProfileRole } from "../../lib/role"; + +interface Props { + resourceToken: "audit" | "clients"; +} + +export function ForbiddenMessage({ resourceToken }: Props) { + const auth = useAuth(); + const rawProfile = auth.user?.profile as Record | undefined; + const role = resolveProfileRole(rawProfile); + + let explanation = t( + "msg.dev.forbidden.default", + "해당 리소스에 접근할 권한이 없습니다. 관리자에게 문의하세요.", + ); + + if (role === "rp_admin") { + explanation = t( + "msg.dev.forbidden.rp_admin", + "RP 관리자는 담당 앱의 리소스만 조회할 수 있습니다.", + ); + } else if (role === "tenant_admin") { + explanation = t( + "msg.dev.forbidden.tenant_admin", + "테넌트 관리자 권한이 올바르게 설정되지 않았거나 만료되었습니다.", + ); + } else if (role === "user" || role === "tenant_member") { + explanation = t( + "msg.dev.forbidden.user", + "일반 사용자는 관리자 화면에 접근할 수 없습니다.", + ); + } + + const title = t("msg.dev.forbidden.title", "{{resource}} 접근 권한 없음", { + resource: + resourceToken === "audit" + ? t("ui.dev.audit.title", "Audit Logs") + : t("ui.dev.clients.registry.subtitle", "연동 앱"), + }); + + return ( +
+ +

{title}

+

{explanation}

+
+ ); +} diff --git a/orgfront/src/components/common/LanguageSelector.tsx b/orgfront/src/components/common/LanguageSelector.tsx new file mode 100644 index 00000000..7f905cd0 --- /dev/null +++ b/orgfront/src/components/common/LanguageSelector.tsx @@ -0,0 +1,53 @@ +import { useState } from "react"; +import { t } from "../../lib/i18n"; + +const LOCALE_STORAGE_KEY = "locale"; +const SUPPORTED_LOCALES = ["ko", "en"] as const; + +type Locale = (typeof SUPPORTED_LOCALES)[number]; + +function resolveLocale(): Locale { + if (typeof window === "undefined") { + return "ko"; + } + + const stored = window.localStorage.getItem(LOCALE_STORAGE_KEY); + if (stored === "ko" || stored === "en") { + return stored; + } + + const pathLocale = window.location.pathname.split("/")[1]; + if (pathLocale === "ko" || pathLocale === "en") { + return pathLocale; + } + + const browserLang = window.navigator.language.toLowerCase(); + return browserLang.startsWith("ko") ? "ko" : "en"; +} + +function LanguageSelector() { + const [locale, setLocale] = useState(resolveLocale()); + + const handleChange = (next: Locale) => { + if (next === locale) { + return; + } + window.localStorage.setItem(LOCALE_STORAGE_KEY, next); + setLocale(next); + window.location.reload(); + }; + + return ( + + ); +} + +export default LanguageSelector; diff --git a/orgfront/src/components/layout/AppLayout.tsx b/orgfront/src/components/layout/AppLayout.tsx new file mode 100644 index 00000000..4e0eb33b --- /dev/null +++ b/orgfront/src/components/layout/AppLayout.tsx @@ -0,0 +1,567 @@ +import { useQuery } from "@tanstack/react-query"; +import { + BadgeCheck, + ChevronDown, + LogOut, + Moon, + NotebookTabs, + ShieldHalf, + Sun, + User as UserIcon, +} from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { useAuth } from "react-oidc-context"; +import { NavLink, Outlet, useLocation, useNavigate } from "react-router-dom"; +import { fetchMe } from "../../features/auth/authApi"; +import { t } from "../../lib/i18n"; +import { resolveProfileRole } from "../../lib/role"; +import { + shouldAttemptSlidingSessionRenew, + shouldAttemptUnlimitedSessionRenew, +} from "../../lib/sessionSliding"; +import LanguageSelector from "../common/LanguageSelector"; +import { Toaster } from "../ui/toaster"; + +const navItems = [ + { + labelKey: "ui.dev.nav.clients", + labelFallback: "Clients", + to: "/clients", + icon: ShieldHalf, + }, + { + labelKey: "ui.dev.nav.audit_logs", + labelFallback: "Audit Logs", + to: "/audit-logs", + icon: NotebookTabs, + }, +]; + +function AppLayout() { + const auth = useAuth(); + const location = useLocation(); + const navigate = useNavigate(); + const profileMenuRef = useRef(null); + const isRenewInFlightRef = useRef(false); + const lastRenewAttemptAtRef = useRef(0); + const lastVisitedRouteRef = useRef(null); + const [theme, setTheme] = useState<"light" | "dark">(() => { + const stored = window.localStorage.getItem("admin_theme"); + return stored === "dark" ? "dark" : "light"; + }); + const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false); + const [isSessionExpiryEnabled, setIsSessionExpiryEnabled] = useState(() => { + const stored = window.localStorage.getItem("baron_session_expiry_enabled"); + return stored !== "false"; + }); + const [nowMs, setNowMs] = useState(() => Date.now()); + + const hasAccessToken = Boolean(auth.user?.access_token); + const { data: profile } = useQuery({ + queryKey: ["userMe"], + queryFn: fetchMe, + enabled: hasAccessToken, + }); + + const handleLogout = () => { + if (window.confirm(t("msg.dev.logout_confirm", "로그아웃 하시겠습니까?"))) { + auth.removeUser(); + navigate("/login"); + } + }; + + useEffect(() => { + const root = document.documentElement; + root.classList.remove("light", "dark"); + if (theme === "light") { + root.classList.add("light"); + } else { + root.classList.add("dark"); + } + window.localStorage.setItem("admin_theme", theme); + }, [theme]); + + useEffect(() => { + const timer = window.setInterval(() => { + setNowMs(Date.now()); + }, 1000); + return () => { + window.clearInterval(timer); + }; + }, []); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + profileMenuRef.current && + !profileMenuRef.current.contains(event.target as Node) + ) { + setIsProfileMenuOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + useEffect(() => { + const maybeRenewSession = async () => { + const now = Date.now(); + if ( + !shouldAttemptSlidingSessionRenew({ + expiresAtSec: auth.user?.expires_at, + nowMs: now, + isEnabled: isSessionExpiryEnabled, + isAuthenticated: auth.isAuthenticated, + isLoading: auth.isLoading, + isRenewInFlight: isRenewInFlightRef.current, + lastAttemptAtMs: lastRenewAttemptAtRef.current, + }) + ) { + return; + } + + isRenewInFlightRef.current = true; + lastRenewAttemptAtRef.current = now; + + try { + await auth.signinSilent(); + } catch (error) { + console.error("세션 자동 연장에 실패했습니다.", error); + } finally { + isRenewInFlightRef.current = false; + } + }; + + const handleUserAction = () => { + void maybeRenewSession(); + }; + + window.addEventListener("pointerdown", handleUserAction); + window.addEventListener("keydown", handleUserAction); + + return () => { + window.removeEventListener("pointerdown", handleUserAction); + window.removeEventListener("keydown", handleUserAction); + }; + }, [ + auth, + auth.isAuthenticated, + auth.isLoading, + auth.user?.expires_at, + isSessionExpiryEnabled, + ]); + + useEffect(() => { + const maybeKeepSessionAlive = async () => { + const now = Date.now(); + if ( + !shouldAttemptUnlimitedSessionRenew({ + expiresAtSec: auth.user?.expires_at, + nowMs: now, + isEnabled: isSessionExpiryEnabled, + isAuthenticated: auth.isAuthenticated, + isLoading: auth.isLoading, + isRenewInFlight: isRenewInFlightRef.current, + lastAttemptAtMs: lastRenewAttemptAtRef.current, + }) + ) { + return; + } + + isRenewInFlightRef.current = true; + lastRenewAttemptAtRef.current = now; + + try { + await auth.signinSilent(); + } catch (error) { + console.error("세션 무제한 유지 갱신에 실패했습니다.", error); + } finally { + isRenewInFlightRef.current = false; + } + }; + + const timer = window.setInterval(() => { + void maybeKeepSessionAlive(); + }, 30_000); + + void maybeKeepSessionAlive(); + + return () => { + window.clearInterval(timer); + }; + }, [ + auth, + auth.isAuthenticated, + auth.isLoading, + auth.user?.expires_at, + isSessionExpiryEnabled, + ]); + + useEffect(() => { + const routeKey = `${location.pathname}${location.search}${location.hash}`; + if (lastVisitedRouteRef.current === null) { + lastVisitedRouteRef.current = routeKey; + return; + } + + if (lastVisitedRouteRef.current === routeKey) { + return; + } + + lastVisitedRouteRef.current = routeKey; + + const now = Date.now(); + if ( + !shouldAttemptSlidingSessionRenew({ + expiresAtSec: auth.user?.expires_at, + nowMs: now, + isEnabled: isSessionExpiryEnabled, + isAuthenticated: auth.isAuthenticated, + isLoading: auth.isLoading, + isRenewInFlight: isRenewInFlightRef.current, + lastAttemptAtMs: lastRenewAttemptAtRef.current, + }) + ) { + return; + } + + isRenewInFlightRef.current = true; + lastRenewAttemptAtRef.current = now; + + void auth + .signinSilent() + .catch((error) => { + console.error("세션 자동 연장에 실패했습니다.", error); + }) + .finally(() => { + isRenewInFlightRef.current = false; + }); + }, [ + auth, + auth.isAuthenticated, + auth.isLoading, + auth.user?.expires_at, + isSessionExpiryEnabled, + location.hash, + location.pathname, + location.search, + ]); + + const toggleTheme = () => { + setTheme((prev) => (prev === "light" ? "dark" : "light")); + }; + + const profileName = + profile?.name?.trim() || + auth.user?.profile?.name?.toString().trim() || + auth.user?.profile?.preferred_username?.toString().trim() || + auth.user?.profile?.nickname?.toString().trim() || + t("ui.dev.profile.unknown_name", "Unknown User"); + const profileEmail = + profile?.email?.trim() || + auth.user?.profile?.email?.toString().trim() || + t("ui.dev.profile.unknown_email", "unknown@example.com"); + const profileInitial = profileName.charAt(0).toUpperCase(); + const currentRole = resolveProfileRole( + auth.user?.profile as Record | undefined, + ); + const displayRoleKey = profile?.role || currentRole; + + const isDevConsoleAllowed = [ + "super_admin", + "tenant_admin", + "rp_admin", + ].includes(currentRole); + const expiresAtSec = auth.user?.expires_at; + const remainingMs = + typeof expiresAtSec === "number" ? expiresAtSec * 1000 - nowMs : null; + const remainingTotalSec = + remainingMs !== null ? Math.max(0, Math.floor(remainingMs / 1000)) : null; + const remainingMinutes = + remainingTotalSec !== null ? Math.floor(remainingTotalSec / 60) : null; + const remainingSeconds = + remainingTotalSec !== null ? remainingTotalSec % 60 : null; + + let sessionToneClass = + "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"; + let sessionText = t("ui.dev.session.active", "세션 활성"); + + if (remainingMs === null) { + sessionToneClass = "border-border bg-card text-muted-foreground"; + sessionText = t("ui.dev.session.unknown", "알 수 없음"); + } else if (remainingMs <= 0) { + sessionToneClass = + "border-rose-500/30 bg-rose-500/10 text-rose-700 dark:text-rose-300"; + sessionText = t("ui.dev.session.expired", "세션 만료"); + } else if ( + remainingMinutes !== null && + remainingSeconds !== null && + remainingMinutes <= 5 + ) { + sessionToneClass = + "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"; + sessionText = t( + "ui.dev.session.expiring", + "만료 임박: {{minutes}}분 {{seconds}}초 남음", + { + minutes: remainingMinutes, + seconds: remainingSeconds, + }, + ); + } else { + sessionText = t( + "ui.dev.session.remaining", + "만료 예정: {{minutes}}분 {{seconds}}초 남음", + { + minutes: remainingMinutes ?? 0, + seconds: remainingSeconds ?? 0, + }, + ); + } + + const handleSessionExpiryToggle = () => { + setIsSessionExpiryEnabled((prev) => { + const next = !prev; + window.localStorage.setItem("baron_session_expiry_enabled", String(next)); + return next; + }); + }; + + return ( +
+ + +
+
+
+
+

+ {t("ui.dev.header.plane", "Dev Plane")} +

+ + {t("ui.dev.header.subtitle", "Manage your applications")} + +
+
+ + + {isSessionExpiryEnabled ? ( + + {sessionText} + + ) : null} +
+ + {isProfileMenuOpen ? ( +
+

+ {t("ui.dev.profile.menu_title", "Account")} +

+
+
+

+ {profileName} +

+

+ {profileEmail} +

+
+
+ + {t( + `ui.admin.role.${displayRoleKey}`, + displayRoleKey.toUpperCase(), + )} + +
+
+ +
+
+
+

+ {t("ui.dev.session.auto_extend", "세션 만료 관리")} +

+

+ {isSessionExpiryEnabled + ? sessionText + : t( + "ui.dev.session.disabled", + "세션 만료 비활성화", + )} +

+
+ +
+
+ + + +
+ ) : null} +
+
+
+
+
+ +
+
+ +
+ ); +} + +export default AppLayout; diff --git a/orgfront/src/components/ui/avatar.tsx b/orgfront/src/components/ui/avatar.tsx new file mode 100644 index 00000000..23e88913 --- /dev/null +++ b/orgfront/src/components/ui/avatar.tsx @@ -0,0 +1,47 @@ +import * as AvatarPrimitive from "@radix-ui/react-avatar"; +import * as React from "react"; +import { cn } from "../../lib/utils"; + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/orgfront/src/components/ui/badge.tsx b/orgfront/src/components/ui/badge.tsx new file mode 100644 index 00000000..97aa8bf9 --- /dev/null +++ b/orgfront/src/components/ui/badge.tsx @@ -0,0 +1,39 @@ +import { type VariantProps, cva } from "class-variance-authority"; +import type * as React from "react"; +import { cn } from "../../lib/utils"; + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + outline: "text-foreground", + muted: "border-border bg-secondary/60 text-muted-foreground", + success: + "border-transparent bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300", + warning: + "border-transparent bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-200", + info: "border-transparent bg-blue-500 text-white hover:bg-blue-500/90", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ); +} + +export { Badge, badgeVariants }; diff --git a/orgfront/src/components/ui/button.tsx b/orgfront/src/components/ui/button.tsx new file mode 100644 index 00000000..ee1a84b4 --- /dev/null +++ b/orgfront/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import { Slot } from "@radix-ui/react-slot"; +import { type VariantProps, cva } from "class-variance-authority"; +import * as React from "react"; +import { cn } from "../../lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-semibold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-background", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + ghost: "hover:bg-accent hover:text-accent-foreground", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + muted: "bg-muted text-muted-foreground hover:bg-muted/80", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-6 text-base", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/orgfront/src/components/ui/card.tsx b/orgfront/src/components/ui/card.tsx new file mode 100644 index 00000000..7048cb12 --- /dev/null +++ b/orgfront/src/components/ui/card.tsx @@ -0,0 +1,72 @@ +import type * as React from "react"; +import { cn } from "../../lib/utils"; + +function Card({ className, ...props }: React.HTMLAttributes) { + return ( +
+ ); +} + +function CardHeader({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +function CardTitle({ + className, + ...props +}: React.HTMLAttributes) { + return ( +

+ ); +} + +function CardDescription({ + className, + ...props +}: React.HTMLAttributes) { + return ( +

+ ); +} + +function CardContent({ + className, + ...props +}: React.HTMLAttributes) { + return

; +} + +function CardFooter({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +export { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, +}; diff --git a/orgfront/src/components/ui/copy-button.tsx b/orgfront/src/components/ui/copy-button.tsx new file mode 100644 index 00000000..8ed058a1 --- /dev/null +++ b/orgfront/src/components/ui/copy-button.tsx @@ -0,0 +1,75 @@ +import { Check, Copy } from "lucide-react"; +import * as React from "react"; +import { cn } from "../../lib/utils"; +import { Button, type ButtonProps } from "./button"; + +interface CopyButtonProps extends ButtonProps { + value: string; + onCopy?: () => void; +} + +export function CopyButton({ + value, + onCopy, + className, + variant = "secondary", + size = "icon", + ...props +}: CopyButtonProps) { + const [hasCopied, setHasCopied] = React.useState(false); + + React.useEffect(() => { + if (hasCopied) { + const timer = setTimeout(() => setHasCopied(false), 1500); + return () => clearTimeout(timer); + } + }, [hasCopied]); + + const copyToClipboard = async () => { + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(value); + } else { + // Fallback for non-secure contexts (HTTP) or missing navigator.clipboard + const textArea = document.createElement("textarea"); + textArea.value = value; + textArea.style.position = "fixed"; + textArea.style.left = "-9999px"; + textArea.style.top = "0"; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + try { + const successful = document.execCommand("copy"); + if (!successful) throw new Error("execCommand copy failed"); + } catch (err) { + console.error("Fallback: Oops, unable to copy", err); + throw err; + } finally { + document.body.removeChild(textArea); + } + } + setHasCopied(true); + if (onCopy) onCopy(); + } catch (err) { + console.error("Failed to copy text: ", err); + } + }; + + return ( + + ); +} diff --git a/orgfront/src/components/ui/input.tsx b/orgfront/src/components/ui/input.tsx new file mode 100644 index 00000000..41955477 --- /dev/null +++ b/orgfront/src/components/ui/input.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; +import { cn } from "../../lib/utils"; + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; + +export { Input }; diff --git a/orgfront/src/components/ui/label.tsx b/orgfront/src/components/ui/label.tsx new file mode 100644 index 00000000..1a555097 --- /dev/null +++ b/orgfront/src/components/ui/label.tsx @@ -0,0 +1,19 @@ +import * as React from "react"; +import { cn } from "../../lib/utils"; + +const Label = React.forwardRef< + HTMLLabelElement, + React.LabelHTMLAttributes +>(({ className, ...props }, ref) => ( +