From 96d0e250eb1bc42e60d32ef3a96af9beb9296a81 Mon Sep 17 00:00:00 2001 From: linmeng <819991304@qq.com> Date: Thu, 30 Jul 2026 10:10:17 +0800 Subject: [PATCH] Revert "Restore staging payment and package management" This reverts commit 751dee39dcd4004a75fda97f4788a8e8fc1d6016, reversing changes made to 43581ac0f75e7f157032503475e878bd53ad161d. --- .../apply-supabase-profile-migrations.yml | 60 - .gitea/workflows/backend-quality-gate.yml | 106 - .gitea/workflows/ci.yml | 46 - .gitea/workflows/deploy-production.yml | 59 - .gitea/workflows/deploy-staging.yml | 105 - .gitea/workflows/migrate-staging-database.yml | 93 - .gitea/workflows/publish-pypi.yml | 43 - .gitea/workflows/release-quality-gate.yml | 40 - .gitea/workflows/test.yml | 42 - ...onfigure-staging-rectification-rollout.yml | 92 - .github/workflows/reset-staging-account.yml | 81 - AGENTS.md | 14 - BLOCKED.md | 2 - CONTEXT.md | 49 - deploy/.env.staging.identity.example | 5 +- deploy/Caddyfile.staging | 17 + deploy/README.md | 13 +- ...configure-staging-rectification-rollout.sh | 166 -- deploy/docker-compose.server.yml | 1 + deploy/railway-api.Dockerfile | 9 +- deploy/railway-web.Dockerfile | 2 +- deploy/reset-staging-account.sh | 290 -- deploy/run-staging-deploy.sh | 23 +- deploy/run-staging-migration.sh | 3 +- deploy/validate-staging-env.sh | 16 +- docs/BUG_HISTORY.md | 83 +- ...arate-agent-failures-from-reply-ratings.md | 7 - ...and-expire-conversation-quality-content.md | 7 - docs/agents/domain.md | 51 - docs/agents/issue-tracker.md | 45 - docs/agents/triage-labels.md | 15 - docs/operations/self-hosted-identity.md | 30 +- .../plans/2026-07-21-self-hosted-identity.md | 2 - ...2026-07-20-supabase-exit-backend-design.md | 2 - .../20260727000000_admin_viewer_identity.sql | 6 - frontend/package-lock.json | 2366 +---------------- frontend/package.json | 6 - frontend/scripts/rectification-v4-worker.mts | 2 + frontend/scripts/staging-image-manifest.mjs | 16 +- frontend/src/app/admin/codes/page.tsx | 215 +- frontend/src/app/admin/layout.tsx | 27 +- frontend/src/app/admin/packages/page.tsx | 5 - frontend/src/app/admin/payments/page.tsx | 5 - frontend/src/app/admin/route.ts | 19 - frontend/src/app/admin/users/page.tsx | 32 - frontend/src/app/api/account/route.ts | 7 +- .../src/app/api/admin/audit-logs/route.ts | 84 - .../src/app/api/admin/codes/[id]/route.ts | 70 - frontend/src/app/api/admin/codes/route.ts | 186 +- .../src/app/api/admin/consultations/route.ts | 79 - .../api/admin/credit-transactions/route.ts | 88 - .../src/app/api/admin/epay-settings/route.ts | 132 - .../app/api/admin/epay-settings/test/route.ts | 46 - frontend/src/app/api/admin/packages/route.ts | 114 - frontend/src/app/api/admin/payments/route.ts | 128 - frontend/src/app/api/admin/session/route.ts | 22 - frontend/src/app/api/admin/users/route.ts | 85 - frontend/src/app/api/auth/[...all]/route.ts | 1 + .../src/app/api/payment/epay/create/route.ts | 53 - .../src/app/api/payment/epay/notify/route.ts | 19 - .../src/app/api/payment/epay/status/route.ts | 5 - .../src/app/api/payment/packages/route.ts | 28 - .../events/[eventId]/revisions/route.ts | 2 - frontend/src/app/api/redeem/route.ts | 1 - frontend/src/app/globals.css | 24 +- frontend/src/app/login/page.tsx | 27 +- frontend/src/app/page.tsx | 76 +- frontend/src/components/admin/admin-app.tsx | 81 - .../components/admin/audit-logs-resource.tsx | 40 - .../src/components/admin/codes-resource.tsx | 167 -- .../admin/consultations-resource.tsx | 40 - .../admin/credit-transactions-resource.tsx | 42 - .../components/admin/package-management.tsx | 163 -- .../components/admin/payment-management.tsx | 271 -- .../src/components/admin/resource-table.tsx | 65 - .../src/components/admin/users-resource.tsx | 36 - frontend/src/components/app-sidebar.tsx | 6 + frontend/src/components/birth-date-picker.tsx | 13 +- frontend/src/components/email-otp-login.tsx | 2 +- frontend/src/components/ui/calendar.tsx | 6 +- frontend/src/lib/admin/auth-policy.ts | 18 - frontend/src/lib/admin/auth.ts | 55 - frontend/src/lib/admin/codes.ts | 79 - frontend/src/lib/admin/database.ts | 46 - frontend/src/lib/admin/http.ts | 53 - frontend/src/lib/admin/providers.ts | 173 -- .../evidence-extractor.ts | 80 +- .../orchestrator.ts | 81 +- .../persistence-contracts.ts | 12 - frontend/src/lib/epay/availability.ts | 43 - frontend/src/lib/epay/config-core.ts | 106 - frontend/src/lib/epay/config.ts | 26 - frontend/src/lib/epay/encryption-core.ts | 46 - frontend/src/lib/epay/encryption.ts | 3 - frontend/src/lib/epay/gateway-policy.ts | 64 - frontend/src/lib/epay/sign.ts | 18 - .../src/lib/rectification-agent/contracts.ts | 194 -- .../rectification-agent/fallback-policy.ts | 13 - .../lib/rectification-agent/feature-policy.ts | 39 - .../opportunity-builder.ts | 113 - .../lib/rectification-agent/orchestrator.ts | 302 --- .../lib/rectification-agent/reasoner-agent.ts | 177 -- .../lib/rectification-agent/renderer-agent.ts | 71 - .../src/lib/rectification-agent/telemetry.ts | 32 - .../lib/rectification-v4/candidate-engine.ts | 151 +- .../src/lib/rectification-v4/case-service.ts | 17 +- .../src/lib/rectification-v4/contracts.ts | 50 +- .../lib/rectification-v4/domain-scorers.ts | 20 +- .../src/lib/rectification-v4/extraction.ts | 219 +- .../src/lib/rectification-v4/fingerprints.ts | 8 +- .../lib/rectification-v4/legacy-projector.ts | 70 - .../src/lib/rectification-v4/memory-store.ts | 29 - .../lib/rectification-v4/opening-question.ts | 16 - .../lib/rectification-v4/question-author.ts | 142 + .../lib/rectification-v4/question-planner.ts | 52 + frontend/src/lib/rectification-v4/store.ts | 8 - .../lib/rectification-v4/supabase-store.ts | 31 +- frontend/src/lib/rectification-v4/worker.ts | 172 +- frontend/src/lib/supabase/admin.ts | 38 +- frontend/src/lib/supabase/server.ts | 11 +- frontend/src/modules/identity/auth-factory.ts | 45 +- frontend/src/modules/identity/auth.ts | 34 +- frontend/src/modules/identity/config.ts | 13 + frontend/src/modules/identity/contracts.ts | 2 + frontend/src/modules/identity/host.ts | 15 +- .../migrations/20260727010000_admin_users.sql | 11 - ...27010000_refine_admin_redemption_audit.sql | 475 ---- .../20260727020000_epay_packages_orders.sql | 69 - .../20260727030000_payment_admin_stats.sql | 23 - ...8010000_conversational_event_semantics.sql | 132 - .../20260728020000_rectification_agent_v5.sql | 883 ------ .../20260729010000_epay_settings.sql | 126 - ...260730010000_admin_payment_permissions.sql | 28 - frontend/tests/account-api.test.ts | 6 - frontend/tests/admin-auth.test.ts | 49 - frontend/tests/admin-contracts.test.ts | 119 - frontend/tests/admin-database.test.ts | 121 - .../tests/admin-payments-contract.test.ts | 94 - frontend/tests/admin-users-contract.test.ts | 47 - .../tests/birth-date-picker-contract.test.ts | 23 +- .../conversational-evidence-extractor.test.ts | 41 +- ...ersational-rectification-component.test.ts | 9 - .../tests/database-local-business.test.ts | 7 - frontend/tests/epay-payment-contract.test.ts | 29 - frontend/tests/epay-settings.test.ts | 183 -- frontend/tests/health-deployment.test.ts | 28 +- frontend/tests/identity-auth-factory.test.ts | 230 +- .../tests/identity-auth-integration.test.ts | 49 +- frontend/tests/identity-config.test.ts | 164 +- frontend/tests/identity-host-routing.test.ts | 119 +- .../tests/identity-login-provider.test.ts | 15 +- .../rectification-agent-contracts.test.ts | 139 - frontend/tests/rectification-agent-v5.test.ts | 346 --- .../tests/rectification-v4-domain.test.ts | 149 +- .../tests/rectification-v4-replay.test.ts | 114 +- .../tests/rectification-v4-service.test.ts | 245 +- ...ectification-v5-migration-contract.test.ts | 83 - .../tests/rectification-v5-test-support.ts | 87 - frontend/tests/sidebar-contract.test.ts | 13 - .../tests/staging-backend-workflows.test.ts | 110 +- frontend/tests/staging-image-manifest.test.ts | 22 - progress.md | 26 - ...sational_rectification_development_v1.json | 4 +- scripts/active_rectification_event_engine.py | 123 +- scripts/active_rectification_events_v4.py | 213 +- scripts/jyotish_api_server.py | 162 +- scripts/jyotish_engine.py | 22 +- scripts/rectification/__init__.py | 1 - scripts/rectification/api_service.py | 71 - .../candidate_feature_service.py | 23 - scripts/rectification/contracts.py | 133 - scripts/rectification/diagnostics_service.py | 91 - scripts/rectification/scoring_service.py | 163 -- skills/birth-time-rectification/SKILL.md | 36 - .../rectification-capability-matrix.json | 11 - .../references/event-schema.md | 3 - .../references/failure-policy.md | 3 - .../references/output-contract.md | 3 - .../references/product-contract.md | 3 - .../references/question-policy.md | 3 - .../references/technique-policy.md | 3 - tests/test_api_server_security.py | 9 +- tests/test_railway_deployment.py | 3 - tests/test_rectification_v5_services.py | 154 -- tests/test_shadbala_complete.py | 6 +- 185 files changed, 2185 insertions(+), 12986 deletions(-) delete mode 100644 .gitea/workflows/apply-supabase-profile-migrations.yml delete mode 100644 .gitea/workflows/backend-quality-gate.yml delete mode 100644 .gitea/workflows/ci.yml delete mode 100644 .gitea/workflows/deploy-production.yml delete mode 100644 .gitea/workflows/deploy-staging.yml delete mode 100644 .gitea/workflows/migrate-staging-database.yml delete mode 100644 .gitea/workflows/publish-pypi.yml delete mode 100644 .gitea/workflows/release-quality-gate.yml delete mode 100644 .gitea/workflows/test.yml delete mode 100644 .github/workflows/configure-staging-rectification-rollout.yml delete mode 100644 .github/workflows/reset-staging-account.yml delete mode 100644 CONTEXT.md delete mode 100755 deploy/configure-staging-rectification-rollout.sh delete mode 100755 deploy/reset-staging-account.sh delete mode 100644 docs/adr/0001-separate-agent-failures-from-reply-ratings.md delete mode 100644 docs/adr/0002-minimize-and-expire-conversation-quality-content.md delete mode 100644 docs/agents/domain.md delete mode 100644 docs/agents/issue-tracker.md delete mode 100644 docs/agents/triage-labels.md delete mode 100644 frontend/db/migrations/20260727000000_admin_viewer_identity.sql delete mode 100644 frontend/src/app/admin/packages/page.tsx delete mode 100644 frontend/src/app/admin/payments/page.tsx delete mode 100644 frontend/src/app/admin/route.ts delete mode 100644 frontend/src/app/admin/users/page.tsx delete mode 100644 frontend/src/app/api/admin/audit-logs/route.ts delete mode 100644 frontend/src/app/api/admin/codes/[id]/route.ts delete mode 100644 frontend/src/app/api/admin/consultations/route.ts delete mode 100644 frontend/src/app/api/admin/credit-transactions/route.ts delete mode 100644 frontend/src/app/api/admin/epay-settings/route.ts delete mode 100644 frontend/src/app/api/admin/epay-settings/test/route.ts delete mode 100644 frontend/src/app/api/admin/packages/route.ts delete mode 100644 frontend/src/app/api/admin/payments/route.ts delete mode 100644 frontend/src/app/api/admin/session/route.ts delete mode 100644 frontend/src/app/api/admin/users/route.ts delete mode 100644 frontend/src/app/api/payment/epay/create/route.ts delete mode 100644 frontend/src/app/api/payment/epay/notify/route.ts delete mode 100644 frontend/src/app/api/payment/epay/status/route.ts delete mode 100644 frontend/src/app/api/payment/packages/route.ts delete mode 100644 frontend/src/components/admin/admin-app.tsx delete mode 100644 frontend/src/components/admin/audit-logs-resource.tsx delete mode 100644 frontend/src/components/admin/codes-resource.tsx delete mode 100644 frontend/src/components/admin/consultations-resource.tsx delete mode 100644 frontend/src/components/admin/credit-transactions-resource.tsx delete mode 100644 frontend/src/components/admin/package-management.tsx delete mode 100644 frontend/src/components/admin/payment-management.tsx delete mode 100644 frontend/src/components/admin/resource-table.tsx delete mode 100644 frontend/src/components/admin/users-resource.tsx delete mode 100644 frontend/src/lib/admin/auth-policy.ts delete mode 100644 frontend/src/lib/admin/auth.ts delete mode 100644 frontend/src/lib/admin/codes.ts delete mode 100644 frontend/src/lib/admin/database.ts delete mode 100644 frontend/src/lib/admin/http.ts delete mode 100644 frontend/src/lib/admin/providers.ts delete mode 100644 frontend/src/lib/epay/availability.ts delete mode 100644 frontend/src/lib/epay/config-core.ts delete mode 100644 frontend/src/lib/epay/config.ts delete mode 100644 frontend/src/lib/epay/encryption-core.ts delete mode 100644 frontend/src/lib/epay/encryption.ts delete mode 100644 frontend/src/lib/epay/gateway-policy.ts delete mode 100644 frontend/src/lib/epay/sign.ts delete mode 100644 frontend/src/lib/rectification-agent/contracts.ts delete mode 100644 frontend/src/lib/rectification-agent/fallback-policy.ts delete mode 100644 frontend/src/lib/rectification-agent/feature-policy.ts delete mode 100644 frontend/src/lib/rectification-agent/opportunity-builder.ts delete mode 100644 frontend/src/lib/rectification-agent/orchestrator.ts delete mode 100644 frontend/src/lib/rectification-agent/reasoner-agent.ts delete mode 100644 frontend/src/lib/rectification-agent/renderer-agent.ts delete mode 100644 frontend/src/lib/rectification-agent/telemetry.ts delete mode 100644 frontend/src/lib/rectification-v4/legacy-projector.ts delete mode 100644 frontend/src/lib/rectification-v4/opening-question.ts create mode 100644 frontend/src/lib/rectification-v4/question-author.ts create mode 100644 frontend/src/lib/rectification-v4/question-planner.ts delete mode 100644 frontend/supabase/migrations/20260727010000_admin_users.sql delete mode 100644 frontend/supabase/migrations/20260727010000_refine_admin_redemption_audit.sql delete mode 100644 frontend/supabase/migrations/20260727020000_epay_packages_orders.sql delete mode 100644 frontend/supabase/migrations/20260727030000_payment_admin_stats.sql delete mode 100644 frontend/supabase/migrations/20260728010000_conversational_event_semantics.sql delete mode 100644 frontend/supabase/migrations/20260728020000_rectification_agent_v5.sql delete mode 100644 frontend/supabase/migrations/20260729010000_epay_settings.sql delete mode 100644 frontend/supabase/migrations/20260730010000_admin_payment_permissions.sql delete mode 100644 frontend/tests/admin-auth.test.ts delete mode 100644 frontend/tests/admin-contracts.test.ts delete mode 100644 frontend/tests/admin-database.test.ts delete mode 100644 frontend/tests/admin-payments-contract.test.ts delete mode 100644 frontend/tests/admin-users-contract.test.ts delete mode 100644 frontend/tests/epay-payment-contract.test.ts delete mode 100644 frontend/tests/epay-settings.test.ts delete mode 100644 frontend/tests/rectification-agent-contracts.test.ts delete mode 100644 frontend/tests/rectification-agent-v5.test.ts delete mode 100644 frontend/tests/rectification-v5-migration-contract.test.ts delete mode 100644 frontend/tests/rectification-v5-test-support.ts delete mode 100644 scripts/rectification/__init__.py delete mode 100644 scripts/rectification/api_service.py delete mode 100644 scripts/rectification/candidate_feature_service.py delete mode 100644 scripts/rectification/contracts.py delete mode 100644 scripts/rectification/diagnostics_service.py delete mode 100644 scripts/rectification/scoring_service.py delete mode 100644 skills/birth-time-rectification/SKILL.md delete mode 100644 skills/birth-time-rectification/assets/rectification-capability-matrix.json delete mode 100644 skills/birth-time-rectification/references/event-schema.md delete mode 100644 skills/birth-time-rectification/references/failure-policy.md delete mode 100644 skills/birth-time-rectification/references/output-contract.md delete mode 100644 skills/birth-time-rectification/references/product-contract.md delete mode 100644 skills/birth-time-rectification/references/question-policy.md delete mode 100644 skills/birth-time-rectification/references/technique-policy.md delete mode 100644 tests/test_rectification_v5_services.py diff --git a/.gitea/workflows/apply-supabase-profile-migrations.yml b/.gitea/workflows/apply-supabase-profile-migrations.yml deleted file mode 100644 index d804daab..00000000 --- a/.gitea/workflows/apply-supabase-profile-migrations.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Apply Supabase profile migrations - -on: - workflow_dispatch: - -concurrency: - group: supabase-profile-migrations - cancel-in-progress: false - -env: - GITEA_SHA: ${{ gitea.sha }} - DEPLOY_HOST: 103.117.123.53 - DEPLOY_PORT: '22000' - DEPLOY_USER: root - DEPLOY_PATH: /opt/jyotisha-app - -jobs: - apply: - runs-on: xiaoxin - timeout-minutes: 15 - steps: - - name: Checkout current Gitea revision - run: | - set -euo pipefail - git init . - git remote remove origin 2>/dev/null || true - git remote add origin https://git.copse.top/root/Jyotisha.git - git fetch --no-tags origin "$GITEA_SHA" main - git checkout --detach --force "$GITEA_SHA" - - name: Verify runner toolchain and require current main - run: | - set -euo pipefail - python3 --version - node --version - npm --version - docker version - test "$(git rev-parse HEAD)" = "$(git ls-remote origin refs/heads/main | awk '{print $1}')" - - name: Configure SSH and apply reviewed files - env: { SSH_PRIVATE_KEY: '${{ secrets.PRODUCTION_SSH_PRIVATE_KEY }}' } - run: | - set -euo pipefail - install -m 700 -d ~/.ssh - printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/jyotisha-production; chmod 600 ~/.ssh/jyotisha-production - printf '%s\n' '[103.117.123.53]:22000 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHQJvN2Mo3Yq8e6ZIK4P2blJ5Vjj0HbknEuk7TyjhMbO' > ~/.ssh/known_hosts - SSH_OPTIONS="-i $HOME/.ssh/jyotisha-production -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" - remote="$DEPLOY_PATH/tmp/profile-migrations/$GITEA_RUN_NUMBER" - ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" "install -m 700 -d '$remote'" - rsync -az -e "ssh $SSH_OPTIONS" frontend/supabase/migrations/20260718*.sql frontend/supabase/migrations/20260721100000_chat_sessions_delete_grant.sql "$DEPLOY_USER@$DEPLOY_HOST:$remote/" - ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" "cd '$DEPLOY_PATH' && REMOTE_DIR='$remote' bash -s" <<'REMOTE' - set -euo pipefail - set +x - trap 'rm -rf "$REMOTE_DIR"' EXIT - set -a; . .env.production; set +a - DB_URL="${SUPABASE_DB_URL:-${DATABASE_URL:-}}" - test -n "$DB_URL" - for sql_file in "$REMOTE_DIR"/*.sql; do - echo "applying $(basename "$sql_file")" - docker run --rm -i postgres:16-alpine psql "$DB_URL" --set ON_ERROR_STOP=1 --quiet < "$sql_file" - done - REMOTE diff --git a/.gitea/workflows/backend-quality-gate.yml b/.gitea/workflows/backend-quality-gate.yml deleted file mode 100644 index 41f464a3..00000000 --- a/.gitea/workflows/backend-quality-gate.yml +++ /dev/null @@ -1,106 +0,0 @@ -name: Deploy staging to test server - -on: - push: - branches: [staging] - -concurrency: - group: staging-deploy - cancel-in-progress: true - -jobs: - deploy: - runs-on: xiaoxin - timeout-minutes: 45 - env: - GITEA_SHA: ${{ gitea.sha }} - REGISTRY_HOST: crpi-d1feco6itet73spp.cn-hongkong.personal.cr.aliyuncs.com - IMAGE_REPOSITORY: crpi-d1feco6itet73spp.cn-hongkong.personal.cr.aliyuncs.com/copse/jyotisha - DEPLOY_HOST: ${{ vars.STAGING_HOST }} - DEPLOY_PORT: ${{ vars.STAGING_PORT }} - DEPLOY_USER: ${{ vars.STAGING_USER }} - DEPLOY_PATH: ${{ vars.STAGING_PATH }} - STAGING_URL: ${{ vars.STAGING_URL }} - - steps: - - name: Checkout staging - run: | - set -euo pipefail - git init . - git remote remove origin 2>/dev/null || true - git remote add origin https://git.copse.top/root/Jyotisha.git - git -c http.connectTimeout=15 -c http.lowSpeedLimit=1024 -c http.lowSpeedTime=30 \ - fetch --depth=1 --no-tags origin "$GITEA_SHA" - git checkout --detach --force "$GITEA_SHA" - - - name: Build and push images - env: - REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} - REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} - run: | - set -euo pipefail - printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOST" --username "$REGISTRY_USERNAME" --password-stdin - docker build -f deploy/railway-api.Dockerfile -t "$IMAGE_REPOSITORY:api-$GITEA_SHA" . - docker build -f deploy/railway-web.Dockerfile -t "$IMAGE_REPOSITORY:web-$GITEA_SHA" . - docker push "$IMAGE_REPOSITORY:api-$GITEA_SHA" - docker push "$IMAGE_REPOSITORY:web-$GITEA_SHA" - docker logout "$REGISTRY_HOST" - - - name: Deploy on test server - env: - REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} - REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} - SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }} - STAGING_KNOWN_HOSTS: ${{ vars.STAGING_KNOWN_HOSTS }} - run: | - set -euo pipefail - install -m 700 -d "$RUNNER_TEMP/staging-ssh" - printf '%s\n' "$SSH_PRIVATE_KEY" | tr -d '\r' > "$RUNNER_TEMP/staging-ssh/id_ed25519" - printf '%s\n' "$STAGING_KNOWN_HOSTS" | tr -d '\r' > "$RUNNER_TEMP/staging-ssh/known_hosts" - chmod 600 "$RUNNER_TEMP/staging-ssh/id_ed25519" "$RUNNER_TEMP/staging-ssh/known_hosts" - - SSH_OPTIONS="-i $RUNNER_TEMP/staging-ssh/id_ed25519 -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$RUNNER_TEMP/staging-ssh/known_hosts" - SCP_OPTIONS="-i $RUNNER_TEMP/staging-ssh/id_ed25519 -P $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$RUNNER_TEMP/staging-ssh/known_hosts" - REMOTE="$DEPLOY_USER@$DEPLOY_HOST" - ARCHIVE="$RUNNER_TEMP/deploy-$GITEA_SHA.tar" - REMOTE_ARCHIVE="/tmp/jyotisha-deploy-$GITEA_SHA.tar" - - tar -cf "$ARCHIVE" deploy - scp $SCP_OPTIONS "$ARCHIVE" "$REMOTE:$REMOTE_ARCHIVE" - printf '%s' "$REGISTRY_PASSWORD" | ssh $SSH_OPTIONS "$REMOTE" "sudo docker login '$REGISTRY_HOST' --username '$REGISTRY_USERNAME' --password-stdin" - ssh $SSH_OPTIONS "$REMOTE" " - set -e - sudo install -d '$DEPLOY_PATH' - sudo tar -xf '$REMOTE_ARCHIVE' -C '$DEPLOY_PATH' - rm -f '$REMOTE_ARCHIVE' - cd '$DEPLOY_PATH' - sudo env \ - API_IMAGE='$IMAGE_REPOSITORY:api-$GITEA_SHA' \ - WEB_IMAGE='$IMAGE_REPOSITORY:web-$GITEA_SHA' \ - GITHUB_SHA='$GITEA_SHA' \ - APP_ENV_FILE='../.env.staging' \ - DATABASE_ENV_FILE='../.env.staging.database' \ - CADDYFILE_PATH='./Caddyfile.staging' \ - SITE_ADDRESS='https://staging.jyotisha.chat' \ - docker compose -p jyotisha-staging --env-file .env.staging \ - -f deploy/docker-compose.server.yml \ - -f deploy/docker-compose.postgres.yml \ - -f deploy/docker-compose.staging.yml \ - pull api web - sudo env \ - API_IMAGE='$IMAGE_REPOSITORY:api-$GITEA_SHA' \ - WEB_IMAGE='$IMAGE_REPOSITORY:web-$GITEA_SHA' \ - GITHUB_SHA='$GITEA_SHA' \ - APP_ENV_FILE='../.env.staging' \ - DATABASE_ENV_FILE='../.env.staging.database' \ - CADDYFILE_PATH='./Caddyfile.staging' \ - SITE_ADDRESS='https://staging.jyotisha.chat' \ - docker compose -p jyotisha-staging --env-file .env.staging \ - -f deploy/docker-compose.server.yml \ - -f deploy/docker-compose.postgres.yml \ - -f deploy/docker-compose.staging.yml \ - up -d --no-build --remove-orphans - sudo docker logout '$REGISTRY_HOST' - " - - curl --fail --silent --show-error --retry 12 --retry-delay 5 "$STAGING_URL/api/health" diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml deleted file mode 100644 index dc13e1b1..00000000 --- a/.gitea/workflows/ci.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Jyotish Skill CI (manual) - -on: - workflow_dispatch: - -jobs: - validate: - runs-on: xiaoxin - timeout-minutes: 30 - env: - GITEA_SHA: ${{ gitea.sha }} - steps: - - name: Checkout current Gitea revision - run: | - set -euo pipefail - git init . - git remote remove origin 2>/dev/null || true - git remote add origin https://git.copse.top/root/Jyotisha.git - git fetch --no-tags origin "$GITEA_SHA" - git checkout --detach --force "$GITEA_SHA" - - name: Verify runner toolchain - run: | - set -euo pipefail - python3 --version - node --version - npm --version - docker version - - name: Install and validate - env: - NEXT_PUBLIC_SUPABASE_URL: https://ci-placeholder.supabase.co - NEXT_PUBLIC_SUPABASE_ANON_KEY: ci-placeholder - run: | - set -euo pipefail - python3 -m venv .venv - export PATH="$PWD/.venv/bin:$PATH" - python -m pip install --upgrade pip - python -m pip install -r requirements.txt -r requirements-dev.txt - npm ci --prefix frontend - ruff check scripts/run_quality_gate.py tests/test_varga_bphs.py tests/test_ashtakavarga_invariants.py tests/test_cli_smoke.py tests/test_yoga_rules_integrity.py - python -m py_compile scripts/*.py jyotish_vedic/*.py mcp_server.py - python scripts/run_quality_gate.py --profile quick --skip-yoga-logic --skip-frontend-runtime - python scripts/commercial_privacy_artifact_scan.py --json - npm test --prefix frontend - npm run lint --prefix frontend - npm run build --prefix frontend - python -m build diff --git a/.gitea/workflows/deploy-production.yml b/.gitea/workflows/deploy-production.yml deleted file mode 100644 index 7ef96484..00000000 --- a/.gitea/workflows/deploy-production.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Deploy production (manual only) - -on: - workflow_dispatch: - -concurrency: - group: production - cancel-in-progress: false - -env: - GITEA_SHA: ${{ gitea.sha }} - DEPLOY_HOST: 103.117.123.53 - DEPLOY_PORT: '22000' - DEPLOY_USER: root - DEPLOY_PATH: /opt/jyotisha-app - -jobs: - deploy: - runs-on: xiaoxin - timeout-minutes: 30 - steps: - - name: Checkout current Gitea revision - run: | - set -euo pipefail - git init . - git remote remove origin 2>/dev/null || true - git remote add origin https://git.copse.top/root/Jyotisha.git - git fetch --no-tags origin "$GITEA_SHA" main - git checkout --detach --force "$GITEA_SHA" - - name: Verify runner toolchain and current main - run: | - set -euo pipefail - python3 --version - node --version - npm --version - docker version - test "$(git rev-parse HEAD)" = "$(git ls-remote origin refs/heads/main | awk '{print $1}')" - - name: Configure pinned production SSH - env: - SSH_PRIVATE_KEY: ${{ secrets.PRODUCTION_SSH_PRIVATE_KEY }} - run: | - set -euo pipefail - install -m 700 -d ~/.ssh - printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/jyotisha-production - chmod 600 ~/.ssh/jyotisha-production - printf '%s\n' '[103.117.123.53]:22000 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHQJvN2Mo3Yq8e6ZIK4P2blJ5Vjj0HbknEuk7TyjhMbO' > ~/.ssh/known_hosts - - name: Sync and rebuild reviewed revision - run: | - set -euo pipefail - SSH_OPTIONS="-i $HOME/.ssh/jyotisha-production -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" - rsync -az --delete --exclude='.git/' --exclude='.env.production' --exclude='frontend/node_modules/' --exclude='frontend/.next/' -e "ssh $SSH_OPTIONS" ./ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/" - ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" "cd '$DEPLOY_PATH' && GITHUB_SHA='$GITEA_SHA' docker compose --env-file .env.production -f deploy/docker-compose.server.yml up -d --build --remove-orphans" - - name: Verify production - run: | - set -euo pipefail - curl -fsS --retry 12 --retry-delay 5 https://jyotisha.chat/login >/dev/null - test "$(curl -sS -o /dev/null -w '%{http_code}' https://jyotisha.chat/api/account)" = 401 - SSH_OPTIONS="-i $HOME/.ssh/jyotisha-production -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes" - ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" "cd '$DEPLOY_PATH' && docker compose --env-file .env.production -f deploy/docker-compose.server.yml exec -T web node -e 'fetch(\"http://api:5200/api/health\").then(async r=>{const b=await r.json();if(!r.ok||b.status!==\"ok\"||b.swisseph_available!==true)process.exit(1)})'" diff --git a/.gitea/workflows/deploy-staging.yml b/.gitea/workflows/deploy-staging.yml deleted file mode 100644 index 697714bf..00000000 --- a/.gitea/workflows/deploy-staging.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: Deploy staging ACR digest (manual rollback or redeploy) - -on: - workflow_dispatch: - inputs: - deploy_sha: - description: Exact tested 40-character staging commit SHA - required: true - type: string - allow_rollback: - description: Explicitly permit a manual rollback - required: true - default: false - type: boolean - -concurrency: - group: staging-mutation - cancel-in-progress: false - -jobs: - deploy: - runs-on: xiaoxin - timeout-minutes: 30 - env: - GITEA_SHA: ${{ gitea.sha }} - REGISTRY_HOST: crpi-d1feco6itet73spp.cn-hongkong.personal.cr.aliyuncs.com - IMAGE_REPOSITORY: crpi-d1feco6itet73spp.cn-hongkong.personal.cr.aliyuncs.com/copse/jyotisha - DEPLOY_HOST: ${{ vars.STAGING_HOST }} - DEPLOY_PORT: ${{ vars.STAGING_PORT }} - DEPLOY_USER: ${{ vars.STAGING_USER }} - DEPLOY_PATH: ${{ vars.STAGING_PATH }} - STAGING_URL: ${{ vars.STAGING_URL }} - STAGING_KNOWN_HOSTS: ${{ vars.STAGING_KNOWN_HOSTS }} - steps: - - name: Checkout trusted main controller - run: | - set -euo pipefail - git init . - git remote remove origin 2>/dev/null || true - git remote add origin https://git.copse.top/root/Jyotisha.git - git fetch --no-tags origin main staging - git checkout --detach --force origin/main - - name: Verify runner toolchain - run: | - set -euo pipefail - python3 --version - node --version - npm --version - docker version - - name: Validate revision and resolve immutable ACR images - id: images - env: - DEPLOY_SHA: ${{ inputs.deploy_sha }} - ALLOW_ROLLBACK: ${{ inputs.allow_rollback }} - REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} - REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} - run: | - set -euo pipefail - [[ "$DEPLOY_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo "deploy_sha must be a full SHA" >&2; exit 1; } - [[ "$ALLOW_ROLLBACK" == true || "$ALLOW_ROLLBACK" == false ]] || exit 1 - staging_sha="$(git ls-remote origin refs/heads/staging | awk '{print $1}')" - [[ "$staging_sha" =~ ^[0-9a-f]{40}$ ]] || exit 1 - if [[ "$ALLOW_ROLLBACK" != true ]]; then [[ "$DEPLOY_SHA" == "$staging_sha" ]] || { echo "forward redeploy must use staging head" >&2; exit 1; }; fi - git cat-file -e "${DEPLOY_SHA}^{commit}" - git merge-base --is-ancestor "$DEPLOY_SHA" origin/main || { echo "revision is not in reviewed main history" >&2; exit 1; } - printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOST" --username "$REGISTRY_USERNAME" --password-stdin - api_digest="$(docker manifest inspect "${IMAGE_REPOSITORY}:api-${DEPLOY_SHA}" --verbose | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("Descriptor", d).get("digest", ""))')" - web_digest="$(docker manifest inspect "${IMAGE_REPOSITORY}:web-${DEPLOY_SHA}" --verbose | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("Descriptor", d).get("digest", ""))')" - [[ "$api_digest" =~ ^sha256:[0-9a-f]{64}$ && "$web_digest" =~ ^sha256:[0-9a-f]{64}$ ]] || exit 1 - printf 'git_sha=%s\napi_digest=%s\nweb_digest=%s\n' "$DEPLOY_SHA" "$api_digest" "$web_digest" > "${RUNNER_TEMP}/manifest.env" - node frontend/scripts/staging-image-manifest.mjs "${RUNNER_TEMP}/manifest.env" "$DEPLOY_SHA" "$IMAGE_REPOSITORY" >> "$GITHUB_OUTPUT" - echo "deploy_sha=$DEPLOY_SHA" >> "$GITHUB_OUTPUT" - - name: Deploy under pinned SSH host identity - env: - SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }} - REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} - REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} - DEPLOY_SHA: ${{ steps.images.outputs.deploy_sha }} - API_IMAGE: ${{ steps.images.outputs.api_image }} - WEB_IMAGE: ${{ steps.images.outputs.web_image }} - ALLOW_ROLLBACK: ${{ inputs.allow_rollback }} - run: | - set -euo pipefail - ssh_root="${RUNNER_TEMP}/staging-ssh" - key_path="$ssh_root/id_ed25519" - known_hosts_path="$ssh_root/known_hosts" - incoming="$DEPLOY_PATH/.incoming/$GITEA_RUN_NUMBER-$GITEA_RUN_ATTEMPT" - install -m 700 -d "$ssh_root" - printf '%s\n' "$SSH_PRIVATE_KEY" | tr -d '\r' > "$key_path" - printf '%s\n' "$STAGING_KNOWN_HOSTS" | tr -d '\r' > "$known_hosts_path" - chmod 600 "$key_path" "$known_hosts_path" - ssh_options=(-i "$key_path" -p "$DEPLOY_PORT" -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o "UserKnownHostsFile=$known_hosts_path") - remote="$DEPLOY_USER@$DEPLOY_HOST" - cleanup() { ssh "${ssh_options[@]}" "$remote" "DOCKER_CONFIG='$incoming/.docker' docker logout '$REGISTRY_HOST' >/dev/null 2>&1 || true; rm -rf -- '$incoming'" >/dev/null 2>&1 || true; docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true; rm -rf -- "$ssh_root"; } - trap cleanup EXIT - ssh "${ssh_options[@]}" "$remote" "install -d -m 700 '$incoming/.docker'" - tar -cf "${RUNNER_TEMP}/deploy.tar" deploy - scp -i "$key_path" -P "$DEPLOY_PORT" -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o "UserKnownHostsFile=$known_hosts_path" "${RUNNER_TEMP}/deploy.tar" "$remote:$incoming/deploy.tar" - ssh "${ssh_options[@]}" "$remote" "tar -xf '$incoming/deploy.tar' -C '$incoming' && rm -f -- '$incoming/deploy.tar'" - previous_sha="$(ssh "${ssh_options[@]}" "$remote" "state='$DEPLOY_PATH/.state/deployed-revision'; if [ -f \"\$state\" ]; then cat \"\$state\"; else id=\$(docker ps -aq --filter 'label=com.docker.compose.project=jyotisha-staging' --filter 'label=com.docker.compose.service=web' | head -n 1); if [ -n \"\$id\" ]; then docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' \"\$id\" | sed -n 's/^GITHUB_SHA=//p' | head -n 1; else printf not-deployed; fi; fi")" - [[ "$previous_sha" == not-deployed || "$previous_sha" =~ ^[0-9a-f]{40}$ ]] || exit 1 - forward_verified=false - if [[ "$previous_sha" != not-deployed && "$previous_sha" != "$DEPLOY_SHA" && "$ALLOW_ROLLBACK" != true ]]; then git cat-file -e "${previous_sha}^{commit}" 2>/dev/null || git fetch origin "$previous_sha"; git merge-base --is-ancestor "$previous_sha" "$DEPLOY_SHA" || { echo "default forward-only deployment refused" >&2; exit 1; }; forward_verified=true; fi - printf '%s' "$REGISTRY_PASSWORD" | ssh "${ssh_options[@]}" "$remote" "DOCKER_CONFIG='$incoming/.docker' docker login '$REGISTRY_HOST' --username '$REGISTRY_USERNAME' --password-stdin" - ssh "${ssh_options[@]}" "$remote" "INCOMING_PATH='$incoming' DEPLOY_PATH='$DEPLOY_PATH' API_IMAGE='$API_IMAGE' WEB_IMAGE='$WEB_IMAGE' DEPLOY_SHA='$DEPLOY_SHA' EXPECTED_PREVIOUS_SHA='$previous_sha' ALLOW_ROLLBACK='$ALLOW_ROLLBACK' FORWARD_REVISION_VERIFIED='$forward_verified' DOCKER_CONFIG='$incoming/.docker' STAGING_URL='$STAGING_URL' bash '$incoming/deploy/run-staging-deploy.sh'" diff --git a/.gitea/workflows/migrate-staging-database.yml b/.gitea/workflows/migrate-staging-database.yml deleted file mode 100644 index 97f78453..00000000 --- a/.gitea/workflows/migrate-staging-database.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: Migrate Staging Database (manual only) - -on: - workflow_dispatch: - inputs: - deploy_sha: - description: Full current staging SHA to migrate - required: true - type: string - -concurrency: - group: staging-mutation - cancel-in-progress: false - -jobs: - migrate: - runs-on: xiaoxin - timeout-minutes: 20 - env: - GITEA_SHA: ${{ gitea.sha }} - REGISTRY_HOST: crpi-d1feco6itet73spp.cn-hongkong.personal.cr.aliyuncs.com - IMAGE_REPOSITORY: crpi-d1feco6itet73spp.cn-hongkong.personal.cr.aliyuncs.com/copse/jyotisha - DEPLOY_HOST: ${{ vars.STAGING_HOST }} - DEPLOY_PORT: ${{ vars.STAGING_PORT }} - DEPLOY_USER: ${{ vars.STAGING_USER }} - DEPLOY_PATH: ${{ vars.STAGING_PATH }} - STAGING_KNOWN_HOSTS: ${{ vars.STAGING_KNOWN_HOSTS }} - steps: - - name: Checkout trusted main controller - run: | - set -euo pipefail - git init . - git remote remove origin 2>/dev/null || true - git remote add origin https://git.copse.top/root/Jyotisha.git - git fetch --no-tags origin main staging - git checkout --detach --force origin/main - - name: Verify runner toolchain - run: | - set -euo pipefail - python3 --version - node --version - npm --version - docker version - - name: Validate revision and resolve migration image - id: image - env: - DEPLOY_SHA: ${{ inputs.deploy_sha }} - REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} - REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} - run: | - set -euo pipefail - [[ "$DEPLOY_SHA" =~ ^[0-9a-f]{40}$ ]] || exit 1 - [[ "$(git ls-remote origin refs/heads/staging | awk '{print $1}')" == "$DEPLOY_SHA" ]] || { echo "migration requires current staging head" >&2; exit 1; } - git cat-file -e "${DEPLOY_SHA}^{commit}" - git merge-base --is-ancestor "$DEPLOY_SHA" origin/main || { echo "revision is not in reviewed main history" >&2; exit 1; } - printf '%s' "$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOST" --username "$REGISTRY_USERNAME" --password-stdin - web_digest="$(docker manifest inspect "${IMAGE_REPOSITORY}:web-${DEPLOY_SHA}" --verbose | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("Descriptor", d).get("digest", ""))')" - [[ "$web_digest" =~ ^sha256:[0-9a-f]{64}$ ]] || exit 1 - echo "web_image=${IMAGE_REPOSITORY}@${web_digest}" >> "$GITHUB_OUTPUT" - echo "deploy_sha=$DEPLOY_SHA" >> "$GITHUB_OUTPUT" - - name: Apply digest-pinned migration under host lock - env: - SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }} - REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }} - REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} - DEPLOY_SHA: ${{ steps.image.outputs.deploy_sha }} - WEB_IMAGE: ${{ steps.image.outputs.web_image }} - run: | - set -euo pipefail - ssh_root="${RUNNER_TEMP}/staging-migration-ssh" - key_path="$ssh_root/id_ed25519" - known_hosts_path="$ssh_root/known_hosts" - incoming="$DEPLOY_PATH/.incoming/$GITEA_RUN_NUMBER-$GITEA_RUN_ATTEMPT" - install -m 700 -d "$ssh_root" - printf '%s\n' "$SSH_PRIVATE_KEY" | tr -d '\r' > "$key_path" - printf '%s\n' "$STAGING_KNOWN_HOSTS" | tr -d '\r' > "$known_hosts_path" - chmod 600 "$key_path" "$known_hosts_path" - ssh_options=(-i "$key_path" -p "$DEPLOY_PORT" -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o "UserKnownHostsFile=$known_hosts_path") - remote="$DEPLOY_USER@$DEPLOY_HOST" - cleanup() { ssh "${ssh_options[@]}" "$remote" "DOCKER_CONFIG='$incoming/.docker' docker logout '$REGISTRY_HOST' >/dev/null 2>&1 || true; rm -rf -- '$incoming'" >/dev/null 2>&1 || true; docker logout "$REGISTRY_HOST" >/dev/null 2>&1 || true; rm -rf -- "$ssh_root"; } - trap cleanup EXIT - ssh "${ssh_options[@]}" "$remote" "install -d -m 700 '$incoming/.docker'" - tar -cf "${RUNNER_TEMP}/deploy.tar" deploy - scp -i "$key_path" -P "$DEPLOY_PORT" -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o "UserKnownHostsFile=$known_hosts_path" "${RUNNER_TEMP}/deploy.tar" "$remote:$incoming/deploy.tar" - ssh "${ssh_options[@]}" "$remote" "tar -xf '$incoming/deploy.tar' -C '$incoming' && rm -f -- '$incoming/deploy.tar'" - previous_sha="$(ssh "${ssh_options[@]}" "$remote" "state='$DEPLOY_PATH/.state/deployed-revision'; if [ -f \"\$state\" ]; then cat \"\$state\"; else id=\$(docker ps -aq --filter 'label=com.docker.compose.project=jyotisha-staging' --filter 'label=com.docker.compose.service=web' | head -n 1); if [ -n \"\$id\" ]; then docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' \"\$id\" | sed -n 's/^GITHUB_SHA=//p' | head -n 1; else printf not-deployed; fi; fi")" - [[ "$previous_sha" == not-deployed || "$previous_sha" =~ ^[0-9a-f]{40}$ ]] || exit 1 - forward_verified=false - if [[ "$previous_sha" != not-deployed && "$previous_sha" != "$DEPLOY_SHA" ]]; then git cat-file -e "${previous_sha}^{commit}" 2>/dev/null || git fetch origin "$previous_sha"; git merge-base --is-ancestor "$previous_sha" "$DEPLOY_SHA" || { echo "migration rollback or divergence refused" >&2; exit 1; }; forward_verified=true; fi - printf '%s' "$REGISTRY_PASSWORD" | ssh "${ssh_options[@]}" "$remote" "DOCKER_CONFIG='$incoming/.docker' docker login '$REGISTRY_HOST' --username '$REGISTRY_USERNAME' --password-stdin" - ssh "${ssh_options[@]}" "$remote" "INCOMING_PATH='$incoming' DEPLOY_PATH='$DEPLOY_PATH' WEB_IMAGE='$WEB_IMAGE' DEPLOY_SHA='$DEPLOY_SHA' EXPECTED_PREVIOUS_SHA='$previous_sha' FORWARD_REVISION_VERIFIED='$forward_verified' DOCKER_CONFIG='$incoming/.docker' bash '$incoming/deploy/run-staging-migration.sh'" - - name: Operator action - run: echo 'Migration complete. Start Deploy staging manually with this exact SHA.' diff --git a/.gitea/workflows/publish-pypi.yml b/.gitea/workflows/publish-pypi.yml deleted file mode 100644 index f127b587..00000000 --- a/.gitea/workflows/publish-pypi.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Publish to PyPI (manual only) - -on: - workflow_dispatch: - -jobs: - build-and-publish: - runs-on: xiaoxin - env: - GITEA_SHA: ${{ gitea.sha }} - steps: - - name: Checkout current Gitea revision - run: | - set -euo pipefail - git init . - git remote remove origin 2>/dev/null || true - git remote add origin https://git.copse.top/root/Jyotisha.git - git fetch --no-tags origin "$GITEA_SHA" - git checkout --detach --force "$GITEA_SHA" - - name: Verify runner toolchain - run: | - set -euo pipefail - python3 --version - node --version - npm --version - docker version - - name: Build and check package - run: | - set -euo pipefail - python3 -m venv .venv - export PATH="$PWD/.venv/bin:$PATH" - python -m pip install --upgrade pip - python -m pip install build twine - python -m build - python -m twine check dist/* - - name: Publish to PyPI - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - set -euo pipefail - export PATH="$PWD/.venv/bin:$PATH" - python -m twine upload --skip-existing dist/* diff --git a/.gitea/workflows/release-quality-gate.yml b/.gitea/workflows/release-quality-gate.yml deleted file mode 100644 index b235097f..00000000 --- a/.gitea/workflows/release-quality-gate.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Jyotish Release Quality Gate (manual only) - -on: - workflow_dispatch: - -jobs: - release-quality-gate: - runs-on: xiaoxin - timeout-minutes: 45 - env: - GITEA_SHA: ${{ gitea.sha }} - steps: - - name: Checkout current Gitea revision - run: | - set -euo pipefail - git init . - git remote remove origin 2>/dev/null || true - git remote add origin https://git.copse.top/root/Jyotisha.git - git fetch --no-tags origin "$GITEA_SHA" - git checkout --detach --force "$GITEA_SHA" - - name: Verify runner toolchain - run: | - set -euo pipefail - python3 --version - node --version - npm --version - docker version - - name: Install dependencies and run release gate - env: - NEXT_PUBLIC_SUPABASE_URL: https://ci-placeholder.supabase.co - NEXT_PUBLIC_SUPABASE_ANON_KEY: ci-placeholder - run: | - set -euo pipefail - python3 -m venv .venv - export PATH="$PWD/.venv/bin:$PATH" - python -m pip install --upgrade pip - python -m pip install -r requirements.txt -r requirements-dev.txt playwright - python -m playwright install --with-deps chromium - npm ci --prefix frontend - python scripts/run_quality_gate.py --profile release diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml deleted file mode 100644 index 9e9c3dfd..00000000 --- a/.gitea/workflows/test.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Jyotish Skill Tests (manual only) - -on: - workflow_dispatch: - -jobs: - test: - runs-on: xiaoxin - env: - GITEA_SHA: ${{ gitea.sha }} - steps: - - name: Checkout current Gitea revision - run: | - set -euo pipefail - git init . - git remote remove origin 2>/dev/null || true - git remote add origin https://git.copse.top/root/Jyotisha.git - git fetch --no-tags origin "$GITEA_SHA" - git checkout --detach --force "$GITEA_SHA" - - name: Verify runner toolchain - run: | - set -euo pipefail - python3 --version - node --version - npm --version - docker version - - name: Install dependencies and run tests - env: - NEXT_PUBLIC_SUPABASE_URL: https://ci-placeholder.supabase.co - NEXT_PUBLIC_SUPABASE_ANON_KEY: ci-placeholder - run: | - set -euo pipefail - python3 -m venv .venv - export PATH="$PWD/.venv/bin:$PATH" - python -m pip install --upgrade pip - python -m pip install -r requirements.txt -r requirements-dev.txt - npm ci --prefix frontend - python -m pytest -vv --maxfail=1 - python tests/run_all.py - npm test --prefix frontend - npm run lint --prefix frontend - npm run build --prefix frontend diff --git a/.github/workflows/configure-staging-rectification-rollout.yml b/.github/workflows/configure-staging-rectification-rollout.yml deleted file mode 100644 index a18fb072..00000000 --- a/.github/workflows/configure-staging-rectification-rollout.yml +++ /dev/null @@ -1,92 +0,0 @@ -name: Configure Staging Rectification Rollout - -on: - workflow_dispatch: - inputs: - expected_deploy_sha: - description: Exact 40-character SHA currently deployed to staging - required: true - type: string - audience: - description: New-case creation audience - required: true - default: paused - type: choice - options: - - paused - - smoke_only - - public - synthetic_smoke_user_ids: - description: Comma-separated canonical UUIDs; required only for smoke_only - required: false - type: string - -permissions: - contents: read - -concurrency: - group: staging-mutation - cancel-in-progress: false - -jobs: - configure: - runs-on: ubuntu-latest - timeout-minutes: 10 - environment: - name: staging - url: ${{ vars.STAGING_URL }} - env: - DEPLOY_HOST: ${{ vars.STAGING_HOST }} - DEPLOY_PORT: ${{ vars.STAGING_PORT }} - DEPLOY_USER: ${{ vars.STAGING_USER }} - DEPLOY_PATH: ${{ vars.STAGING_PATH }} - STAGING_URL: ${{ vars.STAGING_URL }} - STAGING_KNOWN_HOSTS: ${{ vars.STAGING_KNOWN_HOSTS }} - EXPECTED_DEPLOY_SHA: ${{ inputs.expected_deploy_sha }} - ROLLOUT_AUDIENCE: ${{ inputs.audience }} - SYNTHETIC_SMOKE_USER_IDS: ${{ inputs.synthetic_smoke_user_ids }} - - steps: - - name: Checkout trusted controller - uses: actions/checkout@v4 - with: - ref: main - persist-credentials: false - - - name: Validate rollout request and staging target - run: | - set -euo pipefail - [[ "$EXPECTED_DEPLOY_SHA" =~ ^[0-9a-f]{40}$ ]] - case "$ROLLOUT_AUDIENCE" in paused|smoke_only|public) ;; *) exit 1 ;; esac - if [ "$ROLLOUT_AUDIENCE" = smoke_only ]; then - [[ "$SYNTHETIC_SMOKE_USER_IDS" =~ ^[0-9a-f-]{36}(,[0-9a-f-]{36})*$ ]] - else - test -z "$SYNTHETIC_SMOKE_USER_IDS" - fi - test "$DEPLOY_HOST" = "118.26.111.127" - test "$DEPLOY_PORT" = "22" - test "$DEPLOY_USER" = "deploy" - test "$DEPLOY_PATH" = "/opt/jyotisha-staging" - test "$STAGING_URL" = "https://staging.jyotisha.chat" - test -n "$STAGING_KNOWN_HOSTS" - bash -n deploy/configure-staging-rectification-rollout.sh - - - name: Configure pinned staging SSH - env: - SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }} - run: | - set -euo pipefail - test -n "$SSH_PRIVATE_KEY" - install -d -m 700 ~/.ssh - printf '%s\n' "$SSH_PRIVATE_KEY" >~/.ssh/jyotisha-staging - chmod 600 ~/.ssh/jyotisha-staging - printf '%s\n' "$STAGING_KNOWN_HOSTS" >~/.ssh/known_hosts - chmod 600 ~/.ssh/known_hosts - - - name: Apply rollout under staging mutation lock - run: | - set -euo pipefail - SSH_OPTIONS="-i $HOME/.ssh/jyotisha-staging -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=10" - ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" \ - "DEPLOY_PATH='$DEPLOY_PATH' EXPECTED_DEPLOY_SHA='$EXPECTED_DEPLOY_SHA' ROLLOUT_AUDIENCE='$ROLLOUT_AUDIENCE' SYNTHETIC_SMOKE_USER_IDS='$SYNTHETIC_SMOKE_USER_IDS' STAGING_URL='$STAGING_URL' bash -s" \ - < deploy/configure-staging-rectification-rollout.sh diff --git a/.github/workflows/reset-staging-account.yml b/.github/workflows/reset-staging-account.yml deleted file mode 100644 index bfc4af47..00000000 --- a/.github/workflows/reset-staging-account.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Reset Staging Account - -on: - workflow_dispatch: - inputs: - expected_deploy_sha: - description: Exact 40-character SHA currently deployed to staging - required: true - type: string - email: - description: Exact staging account email - required: true - type: string - confirmation: - description: Type RESET followed by a space and the exact email - required: true - type: string - -permissions: - contents: read - -concurrency: - group: staging-mutation - cancel-in-progress: false - -jobs: - reset: - runs-on: ubuntu-latest - timeout-minutes: 10 - environment: - name: staging - url: ${{ vars.STAGING_URL }} - env: - DEPLOY_HOST: ${{ vars.STAGING_HOST }} - DEPLOY_PORT: ${{ vars.STAGING_PORT }} - DEPLOY_USER: ${{ vars.STAGING_USER }} - DEPLOY_PATH: ${{ vars.STAGING_PATH }} - STAGING_KNOWN_HOSTS: ${{ vars.STAGING_KNOWN_HOSTS }} - EXPECTED_DEPLOY_SHA: ${{ inputs.expected_deploy_sha }} - RESET_EMAIL: ${{ inputs.email }} - RESET_CONFIRMATION: ${{ inputs.confirmation }} - - steps: - - name: Checkout trusted controller - uses: actions/checkout@v4 - with: - ref: main - persist-credentials: false - - - name: Validate account reset request and staging target - run: | - set -euo pipefail - [[ "$EXPECTED_DEPLOY_SHA" =~ ^[0-9a-f]{40}$ ]] - [[ "$RESET_EMAIL" =~ ^[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,63}$ ]] - test "$RESET_CONFIRMATION" = "RESET $RESET_EMAIL" - test "$DEPLOY_HOST" = "118.26.111.127" - test "$DEPLOY_PORT" = "22" - test "$DEPLOY_USER" = "deploy" - test "$DEPLOY_PATH" = "/opt/jyotisha-staging" - test -n "$STAGING_KNOWN_HOSTS" - bash -n deploy/reset-staging-account.sh - - - name: Configure pinned staging SSH - env: - SSH_PRIVATE_KEY: ${{ secrets.STAGING_SSH_PRIVATE_KEY }} - run: | - set -euo pipefail - test -n "$SSH_PRIVATE_KEY" - install -d -m 700 ~/.ssh - printf '%s\n' "$SSH_PRIVATE_KEY" >~/.ssh/jyotisha-staging - chmod 600 ~/.ssh/jyotisha-staging - printf '%s\n' "$STAGING_KNOWN_HOSTS" >~/.ssh/known_hosts - chmod 600 ~/.ssh/known_hosts - - - name: Reset one staging account under host lock - run: | - set -euo pipefail - SSH_OPTIONS="-i $HOME/.ssh/jyotisha-staging -p $DEPLOY_PORT -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o ServerAliveInterval=30 -o ServerAliveCountMax=10" - ssh $SSH_OPTIONS "$DEPLOY_USER@$DEPLOY_HOST" \ - "DEPLOY_PATH='$DEPLOY_PATH' EXPECTED_DEPLOY_SHA='$EXPECTED_DEPLOY_SHA' RESET_EMAIL='$RESET_EMAIL' RESET_CONFIRMATION='$RESET_CONFIRMATION' bash -s" \ - < deploy/reset-staging-account.sh diff --git a/AGENTS.md b/AGENTS.md index ced21830..b77987c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -153,17 +153,3 @@ Deployment safety rules: 4. 若当轮只能诊断或被阻塞,也要把已确认事实写成 `investigating` 或 `blocked`,不得编造根因或提前标记 `resolved`。 5. `resolved` 必须有与风险相称的证据:至少一个针对性回归测试;生产问题还必须有脱敏后的迁移、部署、健康检查或 smoke 证据。 6. Bug 历史严禁写入姓名、出生资料、邮箱、用户/案例 ID、Cookie、JWT、密码、密钥、完整请求体或模型原文。 - -## Agent skills - -### Issue tracker - -Issues and PRDs are tracked in this repository's GitHub Issues using the `gh` CLI. See `docs/agents/issue-tracker.md`. - -### Triage labels - -Triage uses the canonical `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, and `wontfix` labels. See `docs/agents/triage-labels.md`. - -### Domain docs - -Domain documentation uses the single-context layout. See `docs/agents/domain.md`. diff --git a/BLOCKED.md b/BLOCKED.md index 22905f0e..dd472a6a 100644 --- a/BLOCKED.md +++ b/BLOCKED.md @@ -1,5 +1,3 @@ # BLOCKED - 真实收信端到端验收:执行环境没有可识别的 staging 测试邮箱/收件箱变量,仓库只记录发信配置而未提供受控测试邮箱。按任务硬规则不使用他人邮箱;代码、测试和部署继续,部署后的注册、验证码登录与忘记密码真实收信步骤待具备受控邮箱后补验。 -- PostgreSQL 事务反向测试:当前执行环境没有 `docker`、`postgres`、`initdb`、`psql`、Podman/Colima/Lima。`frontend/tests/admin-database.test.ts` 已实现审计触发器故意失败并断言兑换码行数仍为 0 的红灯证据,但本地执行在启动 fixture 前以 `spawnSync docker ENOENT` 阻塞;交由 exact-SHA staging quality gate 的 Docker 环境运行。全量 `npm test` 因同一缺失 Docker 共阻塞 11 项数据库/部署测试,另有 1 项既有真实 DOM 测试因缺 Playwright headless Chromium 阻塞;其余 1031 项通过,skipped/todo=0。 -- staging 两角色浏览器冒烟:已确认受控 admin 测试账号存在且是 `user,admin`,但当前执行环境没有其密码或已登录会话;也未提供受控 viewer 账号。不得读取/猜测凭据或使用他人账号。已完成匿名 shell、5 个资源 401、写请求 401 的服务端冒烟;admin/viewer 登录后浏览器冒烟待授权人员提供受控会话后补验。 diff --git a/CONTEXT.md b/CONTEXT.md deleted file mode 100644 index e34f2c1c..00000000 --- a/CONTEXT.md +++ /dev/null @@ -1,49 +0,0 @@ -# Jyotisha 产品领域 - -本上下文定义 Jyotisha Agent 对话、回复质量与后台排错所使用的统一业务语言。 - -## Language - -**Agent 会话**: -用户与某一种 Jyotisha Agent 持续交互的容器,例如普通咨询或出生时间校正。 -_避免使用_:聊天记录、咨询(用于泛指所有 Agent 场景时) - -**Agent 对话轮次**: -在 Agent 会话中,从用户输入触发 Agent 生成一条回复开始,到 Agent 完整回复或该次回复失败为止的一次交互。 -_避免使用_:单条消息、一轮对话 - -**Agent 执行尝试**: -使用独立请求标识执行一个 Agent 对话轮次的一次尝试;重试同一轮次会产生新的尝试。 -_避免使用_:重复消息、同一请求 - -**Agent 执行故障**: -Agent 执行尝试因技术异常未能正常产出完整回复。未登录、余额不足和参数不合法等预期业务拒绝不属于执行故障。 -_避免使用_:所有失败请求、报错 - -**未完成回复**: -Agent 执行故障发生前已经展示给用户、但未正常结束的 Agent 输出。 -_避免使用_:正常回复、可评价回复 - -**故障诊断摘要**: -面向管理员的结构化脱敏故障说明,可用于定位执行阶段和失败类型,但不包含敏感原始诊断内容。 -_避免使用_:原始异常、完整日志 - -**故障上下文快照**: -为排查 Agent 执行故障而保留的故障轮次及该次执行实际使用的近期上下文,不等同于完整会话副本。 -_避免使用_:完整聊天记录、错误消息 - -**回复评价**: -用户针对一条完整 Agent 回复提交的当前正向或负向质量判断。评价属于具体回复,而不是整个 Agent 会话。 -_避免使用_:会话评分、点赞记录 - -**不满意原因**: -负向回复评价附带的一个或多个原因分类,可包含用户补充说明。 -_避免使用_:投诉、差评文本 - -**对话质量记录**: -管理后台中供管理员排查或审阅的一项 Agent 执行故障或负向回复评价。 -_避免使用_:聊天日志、客服工单 - -**处理状态**: -对话质量记录的内部处理进度,取值为待处理、处理中、已解决或忽略。 -_避免使用_:用户反馈状态、通知状态 diff --git a/deploy/.env.staging.identity.example b/deploy/.env.staging.identity.example index fb5f7365..c74373bb 100644 --- a/deploy/.env.staging.identity.example +++ b/deploy/.env.staging.identity.example @@ -3,19 +3,20 @@ APP_ENV_FILE=../.env.staging CADDYFILE_PATH=./Caddyfile.staging SITE_ADDRESS=https://staging.jyotisha.chat +ADMIN_SITE_ADDRESS=https://admin.staging.jyotisha.chat # Staging-only cutover: identity and business data both use the private local # PostgreSQL service. Production remains on Supabase until a separate cutover. AUTH_PROVIDER=self-hosted SELF_HOSTED_IDENTITY_ENABLED=true AUTH_USER_ORIGIN=https://staging.jyotisha.chat +AUTH_ADMIN_ORIGIN=https://admin.staging.jyotisha.chat IDENTITY_DATABASE_URL=postgresql://identity_runtime:@postgres:5432/jyotisha APP_DATABASE_URL=postgresql://app_runtime:@postgres:5432/jyotisha ADMIN_DATABASE_URL=postgresql://admin_runtime:@postgres:5432/jyotisha BETTER_AUTH_USER_SECRET= +BETTER_AUTH_ADMIN_SECRET= RESEND_API_KEY= RESEND_FROM_EMAIL=Jyotisha Staging ADMIN_EMAILS= -EPAY_CONFIG_ENCRYPTION_KEY= -EPAY_CHAT_ENABLED=false JYOTISH_DYNAMIC_RECTIFICATION_TOKEN= diff --git a/deploy/Caddyfile.staging b/deploy/Caddyfile.staging index 66bbb59c..0daab86e 100644 --- a/deploy/Caddyfile.staging +++ b/deploy/Caddyfile.staging @@ -1,4 +1,21 @@ {$SITE_ADDRESS:https://staging.jyotisha.chat} { encode zstd gzip + + @adminPaths path /admin /admin/* /api/admin/* + respond @adminPaths "Not found" 404 + reverse_proxy web:3000 } + +{$ADMIN_SITE_ADDRESS:https://admin.staging.jyotisha.chat} { + encode zstd gzip + + @adminRoot path / + redir @adminRoot /admin/codes 302 + + @adminSurface path /login /admin /admin/* /api/admin/* /api/auth/* /_next/* /jyotish-logo.png /favicon.ico + handle @adminSurface { + reverse_proxy web:3000 + } + respond "Not found" 404 +} diff --git a/deploy/README.md b/deploy/README.md index 3b36b0af..c14ce6c3 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -73,14 +73,6 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=... SUPABASE_SERVICE_ROLE_KEY=... ADMIN_EMAILS=... -# Required to save/read database-backed 易支付 settings. Base64 decoding must -# produce exactly 32 random bytes. Generate independently; never reuse auth keys. -EPAY_CONFIG_ENCRYPTION_KEY= -# Legacy EPAY_GATEWAY_URL / EPAY_PID / EPAY_KEY / EPAY_NOTIFY_URL / -# EPAY_RETURN_URL / EPAY_SITE_NAME remain fallback-only when no database row exists. -# Online packages stay hidden by default; only explicit true enables the fallback. -EPAY_CHAT_ENABLED=false - # Conversational birth-time rectification rollout controls. # Keep migrations false until the ordered database gate below has passed. RECTIFICATION_PRICE_CREDITS=3 @@ -184,9 +176,10 @@ The staging env file must include these non-secret selectors so Compose cannot f APP_ENV_FILE=../.env.staging CADDYFILE_PATH=./Caddyfile.staging SITE_ADDRESS=https://staging.jyotisha.chat +ADMIN_SITE_ADDRESS=https://admin.staging.jyotisha.chat ``` -Staging is fully self-hosted: set `AUTH_PROVIDER=self-hosted` and `SELF_HOSTED_IDENTITY_ENABLED=true`. Add the three role-specific server-only database URLs, the single `AUTH_USER_ORIGIN` and `BETTER_AUTH_USER_SECRET`, and staging-only Resend settings listed in `deploy/.env.staging.identity.example`. The main-site Better Auth user session is also used by `/admin`; persisted `identity.users.role=admin` is the only self-hosted backend role, while `viewer` and ordinary users are denied. Browser code uses same-origin APIs; it receives neither database credentials nor Supabase keys. Production remains on Supabase and is not changed by the staging workflow. See `docs/operations/self-hosted-identity.md` for validation and rollback commands. +Staging is fully self-hosted: set `AUTH_PROVIDER=self-hosted` and `SELF_HOSTED_IDENTITY_ENABLED=true`. Add the three role-specific server-only database URLs, separate user/admin Better Auth secrets, origins, and staging-only Resend settings listed in `deploy/.env.staging.identity.example`. Browser code uses same-origin APIs; it receives neither database credentials nor Supabase keys. Production remains on Supabase and is not changed by the staging workflow. See `docs/operations/self-hosted-identity.md` for validation and rollback commands. After source sync and before `up`, the workflow validates `.env.staging` mode/selectors, explicitly pins the three staging selectors against ambient shell overrides, and runs `docker compose --env-file .env.staging -f deploy/docker-compose.server.yml config --quiet`. For later manual inspections, run the same checks only after the tracked deployment files exist on the server. Do not use a manual gate run from `main` as the first publishing path: publishing requires a successful push to `staging`, while manual `Deploy staging` requires a successful gate run for the exact SHA. @@ -200,8 +193,6 @@ After source sync and before `up`, the workflow validates `.env.staging` mode/se 6. If the read-only checker reports a pending migration, stop app deployment and run `Migrate Staging Database` manually with the same full SHA; a successful migration re-dispatches `Deploy staging` with that same SHA. 7. Confirm `https://staging.jyotisha.chat/api/health` reports the exact SHA and private API health. -After the exact-SHA deployment and migrations are verified, use the manual `Configure Staging Rectification Rollout` workflow to change new-case creation. Supply the SHA currently reported by `/api/health`; choose `public` to open all staging accounts, `smoke_only` with canonical test-account UUIDs for a canary, or `paused` to close creation. The workflow updates only the four `RECTIFICATION_V3_*` rollout variables under the shared host lock, recreates `web` and `rectification-v4-worker` with the already deployed image, and rolls back the env file if health does not match the requested audience. Do not edit or print `.env.staging` through CI logs. - Application rollback uses the same workflow: manually dispatch `Deploy staging` from the `main` controller with a previous known-good full SHA that has a successful `Staging Backend Quality Gate` run, and explicitly set `allow_rollback=true`. Normal and migration-triggered deployments reject stale, divergent, or backward revisions. Rollback still consumes the selected gate run's digest manifest and is supported only during that artifact's 30-day retention window; after expiry, stop and prepare a separately reviewed republish/recovery change rather than substituting a mutable tag or assuming the old run can still be rerun. Database migrations are separate and are not rolled back by an application deployment. Restore a staging database backup before running any destructive migration rehearsal. Inspect staging without printing secrets: diff --git a/deploy/configure-staging-rectification-rollout.sh b/deploy/configure-staging-rectification-rollout.sh deleted file mode 100755 index c276877b..00000000 --- a/deploy/configure-staging-rectification-rollout.sh +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -set +x - -required=(DEPLOY_PATH EXPECTED_DEPLOY_SHA ROLLOUT_AUDIENCE STAGING_URL) -for key in "${required[@]}"; do - if [ -z "${!key:-}" ]; then - echo "required staging rollout input is missing: $key" >&2 - exit 1 - fi -done - -sha_pattern='^[0-9a-f]{40}$' -uuid_pattern='^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' -[[ "$EXPECTED_DEPLOY_SHA" =~ $sha_pattern ]] || { - echo "invalid expected deployment SHA" >&2 - exit 1 -} -case "$ROLLOUT_AUDIENCE" in - paused|smoke_only|public) ;; - *) echo "invalid rollout audience" >&2; exit 1 ;; -esac - -smoke_user_ids="${SYNTHETIC_SMOKE_USER_IDS:-}" -if [ "$ROLLOUT_AUDIENCE" = "smoke_only" ]; then - [ -n "$smoke_user_ids" ] || { - echo "smoke_only requires at least one synthetic user UUID" >&2 - exit 1 - } - IFS=',' read -ra smoke_users <<<"$smoke_user_ids" - for user_id in "${smoke_users[@]}"; do - [[ "$user_id" =~ $uuid_pattern ]] || { - echo "invalid synthetic smoke user UUID" >&2 - exit 1 - } - done -else - [ -z "$smoke_user_ids" ] || { - echo "synthetic smoke users are only valid for smoke_only" >&2 - exit 1 - } -fi - -state_directory="$DEPLOY_PATH/.state" -env_file="$DEPLOY_PATH/.env.staging" -install -d -m 700 "$state_directory" -exec 9>"$state_directory/mutation.lock" -flock -n 9 || { - echo "another staging mutation holds the host lock" >&2 - exit 75 -} - -compose_files=( - -f deploy/docker-compose.server.yml - -f deploy/docker-compose.postgres.yml - -f deploy/docker-compose.staging.yml -) - -[ -f "$env_file" ] || { - echo "staging environment file is missing" >&2 - exit 1 -} -current_sha="$(<"$state_directory/deployed-revision")" -[ "$current_sha" = "$EXPECTED_DEPLOY_SHA" ] || { - echo "deployed staging revision does not match the approved rollout SHA" >&2 - exit 1 -} - -case "$ROLLOUT_AUDIENCE" in - public) - creation_enabled=true - smoke_sha="$EXPECTED_DEPLOY_SHA" - smoke_user_ids="" - ;; - smoke_only) - creation_enabled=true - smoke_sha="" - ;; - paused) - creation_enabled=false - smoke_sha="" - smoke_user_ids="" - ;; -esac - -backup="$(mktemp "$state_directory/rectification-rollout-backup.XXXXXX")" -temporary="$(mktemp "$DEPLOY_PATH/.env.staging.rollout.XXXXXX")" -declare -a compose=() -cleanup() { rm -f -- "$backup" "$temporary"; } -rollback() { - local status=$? - cp -p -- "$backup" "$env_file" - if [ "${#compose[@]}" -gt 0 ]; then - "${compose[@]}" up -d --no-build --pull never --force-recreate --no-deps web rectification-v4-worker >/dev/null 2>&1 || true - fi - exit "$status" -} -trap cleanup EXIT -cp -p -- "$env_file" "$backup" - -awk \ - -v create="$creation_enabled" \ - -v migrations="true" \ - -v smoke_sha="$smoke_sha" \ - -v smoke_users="$smoke_user_ids" ' -BEGIN { - values["RECTIFICATION_V3_CREATE_ENABLED"] = create - values["RECTIFICATION_V3_MIGRATIONS_READY"] = migrations - values["RECTIFICATION_V3_SYNTHETIC_SMOKE_SHA"] = smoke_sha - values["RECTIFICATION_V3_SYNTHETIC_SMOKE_USER_IDS"] = smoke_users -} -{ - split($0, parts, "=") - key = parts[1] - if (key in values) { - if (!(key in written)) print key "=" values[key] - written[key] = 1 - next - } - print -} -END { - for (key in values) if (!(key in written)) print key "=" values[key] -} -' "$env_file" >"$temporary" -chmod 600 "$temporary" - -cd "$DEPLOY_PATH" -bash deploy/validate-staging-env.sh "$temporary" staging.jyotisha.chat deploy/Caddyfile.staging -mv -f -- "$temporary" "$env_file" -trap rollback ERR - -web_container="$(docker ps -aq --filter 'label=com.docker.compose.project=jyotisha-staging' --filter 'label=com.docker.compose.service=web' | head -n 1)" -[ -n "$web_container" ] || { - echo "staging web container is missing" >&2 - false -} -export WEB_IMAGE="$(docker inspect --format '{{.Config.Image}}' "$web_container")" -export APP_ENV_FILE='../.env.staging' -export DATABASE_ENV_FILE='../.env.staging.database' -export CADDYFILE_PATH='./Caddyfile.staging' -export SITE_ADDRESS='https://staging.jyotisha.chat' -export GITHUB_SHA="$EXPECTED_DEPLOY_SHA" -compose=(docker compose -p jyotisha-staging --env-file .env.staging "${compose_files[@]}") - -"${compose[@]}" config --quiet -"${compose[@]}" up -d --no-build --pull never --force-recreate --no-deps web rectification-v4-worker - -health="" -for _ in $(seq 1 30); do - health="$(curl --fail --silent --show-error "$STAGING_URL/api/health" 2>/dev/null || true)" - expected_ready=false - [ "$ROLLOUT_AUDIENCE" = public ] && expected_ready=true - if grep -Fq "\"gitCommit\":\"$EXPECTED_DEPLOY_SHA\"" <<<"$health" && - grep -Fq "\"creationAudience\":\"$ROLLOUT_AUDIENCE\"" <<<"$health" && - grep -Fq "\"readyForNewCases\":$expected_ready" <<<"$health"; then - trap - ERR - printf 'rectification rollout audience=%s deployed_sha=%s ready_for_new_cases=%s\n' \ - "$ROLLOUT_AUDIENCE" "$EXPECTED_DEPLOY_SHA" "$([ "$ROLLOUT_AUDIENCE" = public ] && echo true || echo false)" - exit 0 - fi - sleep 2 -done - -echo "staging rollout health verification failed" >&2 -false diff --git a/deploy/docker-compose.server.yml b/deploy/docker-compose.server.yml index 04222ca2..2ceab51d 100644 --- a/deploy/docker-compose.server.yml +++ b/deploy/docker-compose.server.yml @@ -53,6 +53,7 @@ services: restart: unless-stopped environment: SITE_ADDRESS: ${SITE_ADDRESS:-https://jyotisha.chat} + ADMIN_SITE_ADDRESS: ${ADMIN_SITE_ADDRESS:-https://admin.staging.jyotisha.chat} ports: - "80:80" - "443:443" diff --git a/deploy/railway-api.Dockerfile b/deploy/railway-api.Dockerfile index 48f8561f..23131945 100644 --- a/deploy/railway-api.Dockerfile +++ b/deploy/railway-api.Dockerfile @@ -1,14 +1,11 @@ -FROM m.daocloud.io/docker.io/library/python:3.12-slim +FROM python:3.12-slim ENV PYTHONUNBUFFERED=1 \ - PIP_NO_CACHE_DIR=1 \ - PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ \ - PIP_DEFAULT_TIMEOUT=60 + PIP_NO_CACHE_DIR=1 WORKDIR /app COPY requirements.txt ./ -RUN sed -i 's|http://deb.debian.org|https://mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources \ - && apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 update \ +RUN apt-get update \ && apt-get install -y --no-install-recommends build-essential \ && python -m pip install -r requirements.txt \ && apt-get purge -y --auto-remove build-essential \ diff --git a/deploy/railway-web.Dockerfile b/deploy/railway-web.Dockerfile index cf44ea5b..5926686d 100644 --- a/deploy/railway-web.Dockerfile +++ b/deploy/railway-web.Dockerfile @@ -1,4 +1,4 @@ -FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/node:22-alpine +FROM node:22-alpine WORKDIR /app/frontend COPY frontend/package.json frontend/package-lock.json ./ diff --git a/deploy/reset-staging-account.sh b/deploy/reset-staging-account.sh deleted file mode 100755 index 844a4050..00000000 --- a/deploy/reset-staging-account.sh +++ /dev/null @@ -1,290 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -set +x - -required=(DEPLOY_PATH EXPECTED_DEPLOY_SHA RESET_EMAIL RESET_CONFIRMATION) -for key in "${required[@]}"; do - if [ -z "${!key:-}" ]; then - echo "required staging account-reset input is missing: $key" >&2 - exit 1 - fi -done - -[[ "$EXPECTED_DEPLOY_SHA" =~ ^[0-9a-f]{40}$ ]] || { - echo "invalid expected deployment SHA" >&2 - exit 1 -} -[[ "$RESET_EMAIL" =~ ^[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,63}$ ]] || { - echo "invalid reset email" >&2 - exit 1 -} -[ "$RESET_CONFIRMATION" = "RESET $RESET_EMAIL" ] || { - echo "account reset confirmation does not match" >&2 - exit 1 -} -[ "$DEPLOY_PATH" = "/opt/jyotisha-staging" ] || { - echo "refusing non-staging deployment path" >&2 - exit 1 -} - -state_directory="$DEPLOY_PATH/.state" -[ -f "$state_directory/deployed-revision" ] || { - echo "staging deployed revision is unavailable" >&2 - exit 1 -} -[ "$(<"$state_directory/deployed-revision")" = "$EXPECTED_DEPLOY_SHA" ] || { - echo "deployed staging revision does not match the approved reset SHA" >&2 - exit 1 -} - -install -d -m 700 "$state_directory" -exec 9>"$state_directory/mutation.lock" -flock -n 9 || { - echo "another staging mutation holds the host lock" >&2 - exit 75 -} - -cd "$DEPLOY_PATH" -compose=(docker compose -p jyotisha-staging -f deploy/docker-compose.postgres.yml) -"${compose[@]}" ps --status running postgres --quiet | grep -q . || { - echo "staging postgres container is not running" >&2 - exit 1 -} - -run_psql() { - "${compose[@]}" exec -T -e RESET_EMAIL="$RESET_EMAIL" postgres sh -ceu ' - exec psql -X -v ON_ERROR_STOP=1 -v target_email="$RESET_EMAIL" \ - -U "$POSTGRES_USER" -d "$POSTGRES_DB" - ' -} - -run_psql <<'SQL' -begin; - -create temporary table reset_snapshot on commit drop as -select - identity_user.id, - identity_user.email, - profile.email as profile_email, - profile.credits, - (select count(*) from identity.accounts value where value.user_id = identity_user.id) as identity_accounts, - (select count(*) from identity.sessions value where value.user_id = identity_user.id) as identity_sessions, - (select count(*) from public.credit_transactions value where value.user_id = identity_user.id) as credit_transactions, - (select count(*) from public.credit_request_cancellations value where value.user_id = identity_user.id) as credit_cancellations, - (select count(*) from public.consultation_requests value where value.user_id = identity_user.id) as consultation_requests, - (select count(*) from public.birth_time_rectification_billing value where value.user_id = identity_user.id) as rectification_billing, - (select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = identity_user.id) as action_receipts, - (select count(*) from public.redemption_codes value where value.redeemed_by = identity_user.id) as redeemed_codes, - (select count(*) from audit.admin_audit_logs value where value.actor_user_id = identity_user.id) as admin_audit_logs -from identity.users identity_user -join auth.users auth_user on auth_user.id = identity_user.id -join public.profiles profile on profile.id = identity_user.id -where lower(btrim(identity_user.email)) = lower(btrim(:'target_email')) - and lower(btrim(auth_user.email)) = lower(btrim(:'target_email')) -for update of identity_user, auth_user, profile; - -do $$ -begin - if (select count(*) from reset_snapshot) <> 1 then - raise exception 'account_not_found_or_identity_bridge_mismatch'; - end if; -end $$; - -select jsonb_build_object( - 'stage', 'preflight', - 'email', snapshot.email, - 'credits', snapshot.credits, - 'identityAccounts', snapshot.identity_accounts, - 'identitySessions', snapshot.identity_sessions, - 'creditTransactions', snapshot.credit_transactions, - 'creditCancellations', snapshot.credit_cancellations, - 'consultationRequests', snapshot.consultation_requests, - 'rectificationBilling', snapshot.rectification_billing, - 'actionReceipts', snapshot.action_receipts, - 'redeemedCodes', snapshot.redeemed_codes, - 'adminAuditLogs', snapshot.admin_audit_logs, - 'chatSessions', (select count(*) from public.chat_sessions value where value.user_id = snapshot.id), - 'chartProfiles', (select count(*) from public.chart_profiles value where value.user_id = snapshot.id), - 'synastryReports', (select count(*) from public.synastry_reports value where value.user_id = snapshot.id), - 'legacyRectificationCases', (select count(*) from public.birth_time_rectification_cases value where value.user_id = snapshot.id), - 'v5RectificationCases', (select count(*) from public.birth_time_rectification_v4_cases value where value.user_id = snapshot.id), - 'v5AgentRuns', (select count(*) from public.birth_time_rectification_agent_runs value where value.user_id = snapshot.id), - 'v5Diagnostics', (select count(*) from public.birth_time_rectification_diagnostics value where value.user_id = snapshot.id), - 'v5Jobs', (select count(*) from public.birth_time_rectification_v4_jobs value where value.user_id = snapshot.id) -) -from reset_snapshot snapshot; - -update public.profiles profile -set name = null, - birth_date = null, - birth_time = null, - country_code = null, - province_code = null, - city_code = null, - district_code = null, - onboarding_payload = null, - onboarding_version = null, - onboarding_generated_at = null, - latitude = null, - longitude = null, - timezone_offset = null, - reported_birth_time = null, - active_birth_time = null, - birth_time_source = null, - birth_time_period = null, - birth_time_clue = null, - uncertainty_before_minutes = null, - uncertainty_after_minutes = null, - birth_time_status = null, - rectification_confidence = null, - rectification_case_id = null, - birth_place_label = null, - birth_place_type = null, - birth_place_provider = null, - birth_place_provider_id = null, - timezone_id = null, - timezone_source = null, - updated_at = pg_catalog.now() -from reset_snapshot snapshot -where profile.id = snapshot.id; - -delete from public.chat_sessions value using reset_snapshot snapshot where value.user_id = snapshot.id; -delete from public.chart_profiles value using reset_snapshot snapshot where value.user_id = snapshot.id; -delete from public.synastry_reports value using reset_snapshot snapshot where value.user_id = snapshot.id; -delete from public.birth_time_rectification_v4_cases value using reset_snapshot snapshot where value.user_id = snapshot.id; -delete from public.birth_time_rectification_cases value using reset_snapshot snapshot where value.user_id = snapshot.id; - -do $$ -begin - if exists ( - select 1 - from reset_snapshot snapshot - join identity.users identity_user on identity_user.id = snapshot.id - join auth.users auth_user on auth_user.id = snapshot.id - join public.profiles profile on profile.id = snapshot.id - where identity_user.email is distinct from snapshot.email - or auth_user.email is distinct from snapshot.email - or profile.email is distinct from snapshot.profile_email - or profile.credits is distinct from snapshot.credits - or (select count(*) from identity.accounts value where value.user_id = snapshot.id) <> snapshot.identity_accounts - or (select count(*) from identity.sessions value where value.user_id = snapshot.id) <> snapshot.identity_sessions - or (select count(*) from public.credit_transactions value where value.user_id = snapshot.id) <> snapshot.credit_transactions - or (select count(*) from public.credit_request_cancellations value where value.user_id = snapshot.id) <> snapshot.credit_cancellations - or (select count(*) from public.consultation_requests value where value.user_id = snapshot.id) <> snapshot.consultation_requests - or (select count(*) from public.birth_time_rectification_billing value where value.user_id = snapshot.id) <> snapshot.rectification_billing - or (select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = snapshot.id) <> snapshot.action_receipts - or (select count(*) from public.redemption_codes value where value.redeemed_by = snapshot.id) <> snapshot.redeemed_codes - or (select count(*) from audit.admin_audit_logs value where value.actor_user_id = snapshot.id) <> snapshot.admin_audit_logs - ) then - raise exception 'preserved_state_changed'; - end if; - - if exists (select 1 from public.chat_sessions value join reset_snapshot snapshot on value.user_id = snapshot.id) - or exists (select 1 from public.chart_profiles value join reset_snapshot snapshot on value.user_id = snapshot.id) - or exists (select 1 from public.synastry_reports value join reset_snapshot snapshot on value.user_id = snapshot.id) - or exists (select 1 from public.birth_time_rectification_cases value join reset_snapshot snapshot on value.user_id = snapshot.id) - or exists (select 1 from public.birth_time_rectification_v4_cases value join reset_snapshot snapshot on value.user_id = snapshot.id) - or exists (select 1 from public.birth_time_rectification_v4_jobs value join reset_snapshot snapshot on value.user_id = snapshot.id) - or exists (select 1 from public.birth_time_rectification_agent_runs value join reset_snapshot snapshot on value.user_id = snapshot.id) - or exists (select 1 from public.birth_time_rectification_diagnostics value join reset_snapshot snapshot on value.user_id = snapshot.id) - or exists (select 1 from public.birth_time_rectification_candidate_feature_snapshots value join reset_snapshot snapshot on value.user_id = snapshot.id) - or exists (select 1 from public.birth_time_rectification_public_messages value join reset_snapshot snapshot on value.user_id = snapshot.id) - or exists (select 1 from public.birth_time_rectification_pending_evidence value join reset_snapshot snapshot on value.user_id = snapshot.id) - or exists ( - select 1 from public.profiles profile join reset_snapshot snapshot on profile.id = snapshot.id - where profile.name is not null or profile.birth_date is not null or profile.birth_time is not null - or profile.country_code is not null or profile.province_code is not null or profile.city_code is not null or profile.district_code is not null - or profile.onboarding_payload is not null or profile.onboarding_version is not null or profile.onboarding_generated_at is not null - or profile.latitude is not null or profile.longitude is not null or profile.timezone_offset is not null - or profile.reported_birth_time is not null or profile.active_birth_time is not null or profile.birth_time_source is not null - or profile.birth_time_period is not null or profile.birth_time_clue is not null - or profile.uncertainty_before_minutes is not null or profile.uncertainty_after_minutes is not null - or profile.birth_time_status is not null or profile.rectification_confidence is not null or profile.rectification_case_id is not null - or profile.birth_place_label is not null or profile.birth_place_type is not null or profile.birth_place_provider is not null - or profile.birth_place_provider_id is not null or profile.timezone_id is not null or profile.timezone_source is not null - ) then - raise exception 'reset_state_not_empty'; - end if; -end $$; - -commit; -SQL - -run_psql <<'SQL' -begin; - -create temporary table postflight_target on commit drop as -select identity_user.id, identity_user.email, profile.credits, - not ( - profile.name is null and profile.birth_date is null and profile.birth_time is null - and profile.country_code is null and profile.province_code is null and profile.city_code is null and profile.district_code is null - and profile.onboarding_payload is null and profile.onboarding_version is null and profile.onboarding_generated_at is null - and profile.latitude is null and profile.longitude is null and profile.timezone_offset is null - and profile.reported_birth_time is null and profile.active_birth_time is null and profile.birth_time_source is null - and profile.birth_time_period is null and profile.birth_time_clue is null - and profile.uncertainty_before_minutes is null and profile.uncertainty_after_minutes is null - and profile.birth_time_status is null and profile.rectification_confidence is null and profile.rectification_case_id is null - and profile.birth_place_label is null and profile.birth_place_type is null and profile.birth_place_provider is null - and profile.birth_place_provider_id is null and profile.timezone_id is null and profile.timezone_source is null - ) as profile_not_reset, - lower(btrim(profile.email)) = lower(btrim(identity_user.email)) as profile_email_matches -from identity.users identity_user -join auth.users auth_user on auth_user.id = identity_user.id - and lower(btrim(auth_user.email)) = lower(btrim(identity_user.email)) -join public.profiles profile on profile.id = identity_user.id -where lower(btrim(identity_user.email)) = lower(btrim(:'target_email')); - -do $$ -declare - target_id uuid; -begin - if (select count(*) from postflight_target) <> 1 then - raise exception 'postflight_account_not_found_or_identity_bridge_mismatch'; - end if; - select id into target_id from postflight_target; - - if exists (select 1 from public.chat_sessions value where value.user_id = target_id) - or exists (select 1 from public.chart_profiles value where value.user_id = target_id) - or exists (select 1 from public.synastry_reports value where value.user_id = target_id) - or exists (select 1 from public.birth_time_rectification_cases value where value.user_id = target_id) - or exists (select 1 from public.birth_time_rectification_v4_cases value where value.user_id = target_id) - or exists (select 1 from public.birth_time_rectification_v4_jobs value where value.user_id = target_id) - or exists (select 1 from public.birth_time_rectification_agent_runs value where value.user_id = target_id) - or exists (select 1 from public.birth_time_rectification_diagnostics value where value.user_id = target_id) - or exists (select 1 from public.birth_time_rectification_candidate_feature_snapshots value where value.user_id = target_id) - or exists (select 1 from public.birth_time_rectification_public_messages value where value.user_id = target_id) - or exists (select 1 from public.birth_time_rectification_pending_evidence value where value.user_id = target_id) - or exists (select 1 from postflight_target where profile_not_reset) then - raise exception 'postflight_reset_state_not_empty'; - end if; -end $$; - -select jsonb_build_object( - 'stage', 'postflight', - 'matchedAccounts', (select count(*) from postflight_target), - 'email', (select email from postflight_target), - 'credits', (select credits from postflight_target), - 'profileEmailMatches', (select profile_email_matches from postflight_target), - 'profileNotReset', (select profile_not_reset from postflight_target), - 'chatSessions', (select count(*) from public.chat_sessions value where value.user_id = (select id from postflight_target)), - 'chartProfiles', (select count(*) from public.chart_profiles value where value.user_id = (select id from postflight_target)), - 'synastryReports', (select count(*) from public.synastry_reports value where value.user_id = (select id from postflight_target)), - 'legacyRectificationCases', (select count(*) from public.birth_time_rectification_cases value where value.user_id = (select id from postflight_target)), - 'v5RectificationCases', (select count(*) from public.birth_time_rectification_v4_cases value where value.user_id = (select id from postflight_target)), - 'v5Jobs', (select count(*) from public.birth_time_rectification_v4_jobs value where value.user_id = (select id from postflight_target)), - 'v5AgentRuns', (select count(*) from public.birth_time_rectification_agent_runs value where value.user_id = (select id from postflight_target)), - 'v5Diagnostics', (select count(*) from public.birth_time_rectification_diagnostics value where value.user_id = (select id from postflight_target)), - 'v5FeatureSnapshots', (select count(*) from public.birth_time_rectification_candidate_feature_snapshots value where value.user_id = (select id from postflight_target)), - 'v5PublicMessages', (select count(*) from public.birth_time_rectification_public_messages value where value.user_id = (select id from postflight_target)), - 'v5PendingEvidence', (select count(*) from public.birth_time_rectification_pending_evidence value where value.user_id = (select id from postflight_target)), - 'identityAccounts', (select count(*) from identity.accounts value where value.user_id = (select id from postflight_target)), - 'identitySessions', (select count(*) from identity.sessions value where value.user_id = (select id from postflight_target)), - 'creditTransactions', (select count(*) from public.credit_transactions value where value.user_id = (select id from postflight_target)), - 'creditCancellations', (select count(*) from public.credit_request_cancellations value where value.user_id = (select id from postflight_target)), - 'consultationRequests', (select count(*) from public.consultation_requests value where value.user_id = (select id from postflight_target)), - 'rectificationBilling', (select count(*) from public.birth_time_rectification_billing value where value.user_id = (select id from postflight_target)), - 'actionReceipts', (select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = (select id from postflight_target)) -); - -commit; -SQL diff --git a/deploy/run-staging-deploy.sh b/deploy/run-staging-deploy.sh index 3b95cb54..580c8dfc 100755 --- a/deploy/run-staging-deploy.sh +++ b/deploy/run-staging-deploy.sh @@ -14,7 +14,7 @@ for key in "${required[@]}"; do done sha_pattern='^[0-9a-f]{40}$' -digest_pattern='^[a-z0-9]([a-z0-9.-]*[a-z0-9])?(:[1-9][0-9]{0,4})?(/[a-z0-9]+([._-][a-z0-9]+)*)+@sha256:[0-9a-f]{64}$' +digest_pattern='^ghcr\.io/jesse-ux/jyotisha-(api|web)@sha256:[0-9a-f]{64}$' image_id_pattern='^sha256:[0-9a-f]{64}$' if [[ ! "$DEPLOY_SHA" =~ $sha_pattern ]] || [[ ! "$API_IMAGE" =~ $digest_pattern ]] || @@ -22,8 +22,6 @@ if [[ ! "$DEPLOY_SHA" =~ $sha_pattern ]] || echo "unsafe staging image identity" >&2 exit 1 fi -api_repository="${API_IMAGE%@sha256:*}" -web_repository="${WEB_IMAGE%@sha256:*}" if [ "$ALLOW_ROLLBACK" != "true" ] && [ "$ALLOW_ROLLBACK" != "false" ]; then echo "invalid rollback authorization" >&2 exit 1 @@ -87,8 +85,8 @@ repo_digest_for_container() { awk -v prefix="$repository@sha256:" 'index($0, prefix) == 1 { print; exit }' } -previous_api_image="$(repo_digest_for_container api "$api_repository")" -previous_web_image="$(repo_digest_for_container web "$web_repository")" +previous_api_image="$(repo_digest_for_container api ghcr.io/jesse-ux/jyotisha-api)" +previous_web_image="$(repo_digest_for_container web ghcr.io/jesse-ux/jyotisha-web)" previous_api_id="" previous_web_id="" if [ -n "$(container_id api)" ]; then @@ -128,6 +126,7 @@ export APP_ENV_FILE='../.env.staging' export DATABASE_ENV_FILE='../.env.staging.database' export CADDYFILE_PATH='./Caddyfile.staging' export SITE_ADDRESS='https://staging.jyotisha.chat' +export ADMIN_SITE_ADDRESS='https://admin.staging.jyotisha.chat' export GITHUB_SHA="$DEPLOY_SHA" "${compose[@]}" config --quiet @@ -185,6 +184,7 @@ verify_container_image rectification-v4-worker "$WEB_IMAGE" "${compose[@]}" exec -T \ -e EXPECTED_SHA="$DEPLOY_SHA" -e STAGING_URL="$STAGING_URL" \ + -e STAGING_ADMIN_URL="https://admin.staging.jyotisha.chat" \ web node --input-type=module <<'NODE' const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); let login; @@ -196,10 +196,15 @@ for (let attempt = 0; attempt < 12; attempt += 1) { await delay(5_000); } if (!login?.ok) process.exit(1); -const adminPage = await fetch(`${process.env.STAGING_URL}/admin`, { redirect: "manual" }); -if (adminPage.status !== 307 || adminPage.headers.get("location") !== "/login") process.exit(1); -const adminApi = await fetch(`${process.env.STAGING_URL}/api/admin/session`); -if (adminApi.status !== 401) process.exit(1); +const adminLogin = await fetch(`${process.env.STAGING_ADMIN_URL}/login`); +if (!adminLogin.ok) process.exit(1); +const adminRoot = await fetch(process.env.STAGING_ADMIN_URL, { redirect: "manual" }); +if ( + adminRoot.status !== 302 || + adminRoot.headers.get("location") !== "/admin/codes" +) process.exit(1); +const adminSession = await fetch(`${process.env.STAGING_ADMIN_URL}/api/auth/get-session`); +if (!adminSession.ok) process.exit(1); const account = await fetch(`${process.env.STAGING_URL}/api/account`); if (account.status !== 401) process.exit(1); const publicHealth = await fetch(`${process.env.STAGING_URL}/api/health`); diff --git a/deploy/run-staging-migration.sh b/deploy/run-staging-migration.sh index 9b342251..00d611f7 100755 --- a/deploy/run-staging-migration.sh +++ b/deploy/run-staging-migration.sh @@ -17,8 +17,7 @@ done echo "unsafe staging migration revision" >&2 exit 1 } -image_pattern='^crpi-d1feco6itet73spp\.cn-hongkong\.personal\.cr\.aliyuncs\.com/copse/jyotisha@sha256:[0-9a-f]{64}$' -[[ "$WEB_IMAGE" =~ $image_pattern ]] || { +[[ "$WEB_IMAGE" =~ ^ghcr\.io/jesse-ux/jyotisha-web@sha256:[0-9a-f]{64}$ ]] || { echo "unsafe staging migration image" >&2 exit 1 } diff --git a/deploy/validate-staging-env.sh b/deploy/validate-staging-env.sh index 807af650..d54605cb 100755 --- a/deploy/validate-staging-env.sh +++ b/deploy/validate-staging-env.sh @@ -41,9 +41,11 @@ require_selector() { require_selector APP_ENV_FILE ../.env.staging require_selector CADDYFILE_PATH ./Caddyfile.staging require_selector SITE_ADDRESS https://staging.jyotisha.chat +require_selector ADMIN_SITE_ADDRESS https://admin.staging.jyotisha.chat require_selector AUTH_PROVIDER self-hosted require_selector SELF_HOSTED_IDENTITY_ENABLED true require_selector AUTH_USER_ORIGIN https://staging.jyotisha.chat +require_selector AUTH_ADMIN_ORIGIN https://admin.staging.jyotisha.chat require_literal() { local key="$1" @@ -86,6 +88,13 @@ if ! [[ "$admin_database_url" =~ ^postgresql://admin_runtime:([A-Za-z0-9._~-]|%[ fi require_literal BETTER_AUTH_USER_SECRET 32 +user_secret="$LITERAL_VALUE" +require_literal BETTER_AUTH_ADMIN_SECRET 32 +admin_secret="$LITERAL_VALUE" +if [ "$user_secret" = "$admin_secret" ]; then + echo "staging identity secrets must be different" >&2 + exit 1 +fi require_literal RESEND_API_KEY 10 require_literal RESEND_FROM_EMAIL 5 if [[ "$LITERAL_VALUE" != *@* ]]; then @@ -97,13 +106,6 @@ if [[ "$LITERAL_VALUE" != *@* ]]; then echo "invalid staging identity setting: ADMIN_EMAILS" >&2 exit 1 fi -require_literal EPAY_CONFIG_ENCRYPTION_KEY 44 -if [ "${#LITERAL_VALUE}" -ne 44 ] || - [[ ! "$LITERAL_VALUE" =~ ^[A-Za-z0-9+/]{43}=$ ]]; then - echo "invalid staging identity setting: EPAY_CONFIG_ENCRYPTION_KEY" >&2 - exit 1 -fi -require_selector EPAY_CHAT_ENABLED false require_literal JYOTISH_DYNAMIC_RECTIFICATION_TOKEN 32 echo "staging environment selectors: valid" diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index b801eb3f..657c8eac 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -1553,77 +1553,28 @@ - 状态:investigating - 首次发现:2026-07-27 -- 最近更新:2026-07-28 -- 影响面:生时校正聊天 Surface、事件语义、后台 Job、候选计算、诊断、Reasoner、Renderer 与持久化主链 +- 最近更新:2026-07-27 +- 影响面:生时校正 V4 聊天界面、历史恢复、模型选择、下一问规划与 staging 验收 - 用户现象:进入生时校正后看到独立的校正面板、证据区域和固定问题;交互不像普通 session,领域也不再根据用户刚讲的经历动态选择。 -- 触发条件:旧 V4 既在界面层使用独立校正结构,又让 `question-planner.ts` 和 `question-author.ts` 直接决定领域顺序与问题文案;模型只负责写下一问,后台没有形成完整 Agent 决策闭环。 -- 根因:产品状态被压缩成“下一问字符串”,事件语义、候选特征、诊断结果、问题机会、模型决策和公开消息之间没有受约束的 durable contract;因此即使替换提示词,系统仍会沿用问卷式控制流,且无法审计模型为何选题或安全重放已完成 Job。 -- 修复:删除旧 `question-planner.ts` 与 `question-author.ts`,将回答处理重构为完整 V5 主链:保存回答并创建后台 Job → Evidence Reconciliation → Candidate Engine / Feature Snapshot → Diagnostics → Opportunity Builder → Bounded Reasoner → Decision Validator → Renderer → Atomic Job Completion。可见层继续复用普通 session 聊天 Surface;Reasoner 只能选择服务端生成的 opportunity 或受约束动作,不能注入分钟、分数、事件或任意问题;Renderer 只表达已验证决定,候选范围不得表述为已确认出生分钟。Agent Run、Public Message、Diagnostics、Feature Snapshot、Pending Evidence 和事件修订均作为一等产物持久化。 -- 验证:67 个 TypeScript 聚焦合同全部通过,覆盖普通 session UI、完整 V5 artifact chain、Reasoner 单次诊断预算、Opportunity 选择、shadow/legacy 隔离和 range-only 输出;7 个 Python 服务合同通过。真实 PostgreSQL 14 已按 V4 → V5 顺序完成 migration dry-run,并跑通 `processing → reasoning → rendering → complete`、五类 artifact 各一条落库和 completed Job 幂等重放。`tsc --noEmit` 未出现 V5 新错误,只剩 `birth-time-journey-engine`、`identity-auth-integration`、`onboarding-route` 三处无关基线错误。当前完成边界为本地可测,尚未提交、推送、迁移 staging 或执行登录态 smoke。 -- 防复发:生时校正不得再次把模型降级为“问题文案生成器”;所有可见动作必须来自 server-owned opportunity,经 bounded reasoner、decision validator 和 renderer 后原子持久化。测试必须同时锁定 legacy/shadow 隔离、artifact 完整性、候选范围边界和 completed-job replay 指纹。 -- 相关记录:BUG-020、BUG-075、BUG-080、BUG-081、BUG-082、BUG-083、BUG-084、BUG-086 -- 修复版本:本地 V5 重构,待提交与 staging 验收 +- 触发条件:V4 页面入口渲染旧式 `RectificationV4Panel` 视觉结构,页面给会话容器添加 `is-rectification`,同时问题规划器按硬编码领域顺序和模板生成下一问。 +- 根因:组件 wrapper 无条件绕过原普通聊天 Surface;普通 session CSS 又显式排除 `is-rectification`;`question-planner.ts` 把教育、迁移、关系、事业、财务、健康压力和家庭写成固定顺序与固定文案,测试还把这些实现细节当成产品合同。 +- 修复:V4 复用普通 session 的消息列表、输入框和模型选择器,并从持久化 turns 恢复完整对话;回答时原子保存所选模型 ID,Worker 将完整 turns、事件台账、日期精度、已追问事件与候选范围交给模型动态生成下一问。确定性 planner 只保留日期修订和开放叙述降级,不再轮询领域或输出固定问卷;候选范围仍不得表述为已确认出生分钟。 +- 验证:聚焦 V4/domain/service/replay/handoff/migration、普通 session UI 合同和 consultation entrypoint 共 59 个测试通过;staging 构建、迁移和登录态 smoke 完成后更新为 resolved 并填写精确提交与部署 SHA。 +- 防复发:可见生时校正必须复用普通聊天 Surface;测试应锁定自然语言消息、turn 恢复、模型 ID 传递和无固定领域控件,不得锁定领域顺序或问题模板。模型只负责选择和表达下一条高信息量问题,证据修订、评分、稳定性门、范围接受、handoff 与扣费继续由确定性后端负责。 +- 相关记录:BUG-020、BUG-075、BUG-080、BUG-081、BUG-082、BUG-083、BUG-084 +- 修复版本:待提交(staging 验收中) ## BUG-086 | 模型下一问可绕过当前事件而跳成领域问卷 - 状态:investigating - 首次发现:2026-07-27 -- 最近更新:2026-07-28 -- 影响面:生时校正 V5 的当前事件延续、问题机会构建、诊断工具预算、模型决策验证和 Job replay +- 最近更新:2026-07-27 +- 影响面:生时校正 V4 的模型提问规划、事件日期补全和 staging 对话体验 - 用户现象:用户回答“2016 年离家去外地上大学”后,下一问直接变成“请说一次影响较大的搬家或长期迁居”,看起来仍按“升学 → 搬家”模板轮询,而没有承接刚才的具体经历。 -- 触发条件:当前事件仍缺必要精度,但旧 Worker 只校验模型返回结构;只要模型输出一个格式合法的新领域问题,就可以绕过当前事件和服务端已知证据缺口。 -- 根因:旧方案把“required continuation”作为给模型的提示,而不是服务器拥有的候选动作和最终决策约束;诊断结果也没有独立工具预算、持久化产物和可回放选择依据,无法阻止合法 JSON 携带错误业务路由。 -- 修复:Opportunity Builder 将未解决的当前目标设为独占路由,并只发布带稳定 ID、目标事件、效用分解和隐私成本的问题机会;Bounded Reasoner 最多执行一次只读诊断,最终只能选择活动 opportunity 或受限状态动作;Decision Validator 拒绝不存在、跨 Case、非活动或越权的机会,也禁止模型直接写问题、分钟、分数和事件。Reasoner 不可用、返回非最终诊断或耗尽预算时走同一确定性 fallback policy;Renderer 根据 validated decision 生成自然语言承接,Worker 再通过单一 completion RPC 原子保存全部产物。 -- 验证:对抗合同覆盖“当前目标独占下一问”“只能选择服务端活动 opportunity”“诊断预算耗尽 fail closed”“模型不得注入问题/分钟/事件/分数”和“Reasoner/Renderer 不可用时确定性降级”。真实 PostgreSQL completed-job replay 已验证:相同完整 payload 指纹返回既有 Case;任一 artifact 改变且指纹不同会抛出 `rectification_v5_replay_payload_mismatch`,不会二次写入或接受漂移结果。当前仅完成本地验证,staging 行为仍待发布后验收。 -- 防复发:当前事件延续必须是服务端 opportunity 所有权规则,而不是 prompt 建议;模型输出即使结构合法,也必须经过 bounded tool budget、active-opportunity lookup、decision validation 和 completion payload hash 四层门控。 +- 触发条件:最新可评分事件只有年份精度,但模型返回新的领域和空 `targetEventId`;Worker 直接接受格式合法的模型结果。 +- 根因:模型提示虽然要求优先延续当前事件,但 Worker 只校验了输出结构,没有把确定性 planner 识别出的必要日期补全当作服务端路由约束;因此模型可越过仍缺月份的当前事件。旧测试只证明模型拿到了完整上下文,没有覆盖模型违反路由建议的情况。 +- 修复:planner 将月份视为足够的首选精度;年份、季度或范围精度仍产生必要的当前事件补全。问题作者收到 `requiredContinuation`,必须围绕该事件自然追问月份或日期;Worker 在信任边界拒绝模型切换事件或领域,并回退到同一事件的开放式日期追问。当前事件达到月份精度后,模型才可根据上下文自由选择下一条高信息量问题,不设领域顺序。 +- 验证:新增用户原句回归,模拟模型错误返回搬家问题,断言 Worker 仍追问“离家去外地上大学”的月份且不出现搬家模板;同时锁定月份精度后模型可自由选题。聚焦 domain/service/replay 共 18 个测试通过;staging 部署与真实登录态 smoke 完成后更新状态。 +- 防复发:模型可以表达和选择下一题,但不能绕过服务端判定的当前事件必要补全;测试必须包含“模型输出合法但路由错误”的对抗用例,不能只测 happy path。 - 相关记录:BUG-075、BUG-085 -- 修复版本:本地 V5 重构,待提交与 staging 验收 - -## BUG-087 | self-hosted staging 管理员看不到独立后台入口 - -- 状态:superseded by BUG-092 -- 首次发现:2026-07-29 -- 最近更新:2026-07-29 -- 影响面:self-hosted staging 账户菜单、`GET /api/account`、独立后台入口;不影响后台独立登录与 `requireAdminSession` -- 用户现象:身份库已持久化 `admin` 或 `viewer` 角色的用户登录主站后,账户菜单不显示后台入口;即使显示旧入口,主站 `/admin` 路径也会返回 404。 -- 触发条件:`AUTH_PROVIDER=self-hosted`,后台部署在与主站不同的 `AUTH_ADMIN_ORIGIN`,用户角色以逗号分隔形式持久化在 `identity.users.role`。 -- 根因:主站 `isAdminUser` 对 self-hosted 模式直接返回 `false`,没有读取持久化角色;侧栏又把入口写死为主站相对路径 `/admin/codes`。既有后台鉴权已按持久化角色执行,但主站入口发现逻辑没有复用同一授权事实,独立域名部署合同也没有进入账户响应。 -- 修复:self-hosted 分支通过现有 `ADMIN_DATABASE_URL` 管理只读连接查询当前用户的 `identity.users.role`,仅 `admin` 或 `viewer` 可见入口,且不使用 `ADMIN_EMAILS` 替代角色授权;`GET /api/account` 在服务端解析身份配置并返回 `AUTH_ADMIN_ORIGIN + /admin/codes`,Supabase 模式继续返回 `/admin/codes`;账户与侧栏类型透传该 URL,并将文案改为“后台管理”。后台独立登录和 `requireAdminSession` 保持不变。 -- 验证:`frontend/tests/admin-contracts.test.ts`、`frontend/tests/admin-users-contract.test.ts`、`frontend/tests/account-api.test.ts`、`frontend/tests/sidebar-contract.test.ts` 锁定持久化角色、独立后台 URL、服务端环境边界和后台写权限门禁;目标 TypeScript、构建与 staging 登录态 smoke 结果另行记录。 -- 防复发:self-hosted 主站入口发现必须以 `identity.users.role` 为授权事实,不能退回邮箱 allowlist;客户端不得读取后台 origin 环境变量或硬编码主站 `/admin` 路径;后台 API 必须继续独立执行 `requireAdminSession`,入口可见性不得被当作授权。 -- 相关记录:BUG-010、BUG-083、BUG-084、BUG-092 -- 复发自:BUG-010 -- 修复版本:已由 BUG-092 的同域单会话架构取代 - -## BUG-092 | self-hosted staging 双域后台与主站会话模型冲突 - -- 状态:resolved -- 首次发现:2026-07-29 -- 最近更新:2026-07-29 -- 影响面:staging Better Auth 配置、后台页面与 API、登录、账户入口、Caddy、部署校验和 smoke;生产配置不变。 -- 用户现象:管理员需要第二套后台域名和浏览器会话才能进入后台,主站登录态不能直接使用;`viewer` 还被当作后台只读角色,与仅数据库 `admin` 可进入的产品合同冲突。 -- 触发条件:self-hosted staging 同时配置用户与后台 origin/secret、Caddy 拆分两个站点,并按 Host 选择 Better Auth 实例。 -- 根因:早期隔离设计把后台浏览器 surface 当成第二套身份系统,导致入口发现、登录、Cookie、部署变量和授权策略重复;同时把入口可见性与 API 权限错误扩展到 `viewer`。 -- 架构决策:后台复用主站 Better Auth user session;`identity.users.role` 的持久化 `admin` 是唯一后台授权事实。Better Auth 插件的 `/api/auth/admin` endpoint 继续在主站 fail-closed `404`,未知 Host 继续 `421`。 -- 修复:删除活动运行时后台 origin/secret 与 `services.admin`,服务端数据 client 和 `requireAdminSession` 统一读取 user session;后台 layout 增加服务端 gate,匿名转 `/login`、非 admin 不渲染;所有后台 API 保留独立 guard,payments/packages 改用 `requireAdminSession`;`isAdminUser`、账户入口和 Refine policy 收敛为 admin-only;登录取消 Host 分流;staging Caddy、Compose、环境校验、部署脚本、工作流和 smoke 收敛为同域。 -- 验证:身份 config/host/auth、admin policy/contracts、account/sidebar/login、部署/工作流与 admin layout/API guard 合同更新;针对性测试、TypeScript、Next build 与 `git diff --check` 结果记录在本次交付报告。生产部署未执行。 -- 防复发:活动运行配置和测试不得重新引入独立后台域名、`AUTH_ADMIN_ORIGIN`、`BETTER_AUTH_ADMIN_SECRET` 或浏览器 admin auth service;`viewer` 对后台入口、页面、读 API 和写 API 均必须为 `403`;入口可见性不能替代 route guard。 -- 相关记录:BUG-010、BUG-083、BUG-084、BUG-087 -- 复发自:BUG-087 -- 修复版本:`435e628806390e7ae138363491e7bae63ee801d4`,staging 已验收 - -## BUG-093 | 后台支付入口分散且界面风格不一致 - -- 状态:investigating -- 首次发现:2026-07-29 -- 最近更新:2026-07-30 -- 影响面:后台 Refine 侧栏、`/admin/payments`、`/admin/packages`、易支付配置与对话页充值入口。 -- 用户现象:支付记录与支付配置占用两个导航项,页面仍使用主站 `standalone-page/admin-header/admin-section` 样式;套餐新增表单常驻页面,后台默认退出入口还会触发登出,管理员难以直接返回对话;对话页支付入口缺少安全默认关闭和服务端创建订单硬门禁。2026-07-29 复发时,Z-Pay 配置不能折叠且占据长页面,后台受全局 `html/body overflow:hidden` 限制无法纵向滚动,套餐 API 与易支付配置 API 仍调用 self-hosted adapter 不支持的 Supabase builder/RPC。2026-07-30 部署 `dd8e2ad9c7e76d0152b4563c43a45b1e26137035` 后,`GET /api/admin/payments` 与套餐管理仍返回 500。 -- 触发条件:进入同域 `/admin` 后管理支付记录或套餐,或点击 Refine 侧栏底部默认 Logout;复发条件为进入支付管理、展开长配置或调用套餐 CRUD / 易支付配置读写。2026-07-30 的数据库权限复发在 `admin_runtime` 通过 `ADMIN_DATABASE_URL` 查询支付表时稳定触发。 -- 根因:首轮支付后台实现依赖 Supabase 专用关联 select、分页、计数和 Admin Auth 查询;self-hosted staging 的本地 PostgreSQL adapter 不支持这些 builder 能力,支付记录因此统一降级为“支付记录服务暂时不可用”。同页套餐设计也不符合最新后台信息架构,易支付配置响应漏投影 `chat_enabled`,chat 创建订单又依赖服务端提交网关后猜测跳转地址,不兼容标准易支付收银台表单页。复发遗漏源于上轮只把支付记录切换到 PostgreSQL,套餐与配置契约测试没有锁定 self-hosted 数据链,且未覆盖聊天全局滚动边界下的后台专用滚动容器。2026-07-30 的直接根因是 `20260727020000_epay_packages_orders.sql` 只向 Supabase 的 `service_role` / `authenticated` 授权,未向 self-hosted 后台实际使用的 `admin_runtime` 授予 `payment_packages`、`payment_orders` 权限,也未添加对应 RLS 策略;因此数据库健康且新 SHA 已部署,后台 SQL 仍被 PostgreSQL 权限门禁拒绝。 -- 修复:支付记录改为通过 `queryAdminRows` 执行参数化 SQL,联表 `public.payment_orders`、`public.payment_packages` 和 `identity.users`,以窗口计数保留分页合同并用独立聚合 SQL输出统计;不再使用 Supabase builder 或 Admin Auth。后台在支付管理之后新增独立“套餐管理”资源和页面,套餐新增、编辑、停用、错误重试及原字段保持完整,支付页只保留概览、Z-Pay(易支付)渠道配置和支付记录。配置读取补回 `chat_enabled` 与 `chatEnabled`。创建订单完成登录、开关、配置、SSRF、套餐和订单校验后,直接返回带 `sign/sign_type` 的标准 `submit.php` 收银台 URL,不服务端请求网关、不返回商户密钥;对话页用浏览器打开该 URL,套餐加载异常显示安全错误,正常 `enabled=false` 仍静默隐藏。复发修复将 Z-Pay 配置改为默认收起的 Ant Design `Collapse`,展开后才显示表单和操作;为 AdminApp 增加 `admin-app-shell` 的 `100dvh` 独立纵向滚动边界而不改聊天全局规则;套餐 CRUD 全部改用 `queryAdminRows` 参数化 SQL、UUID 校验、`returning` 与 404;易支付读取仅在 PostgreSQL `42P01` 时回退环境变量,保存直接参数化调用 `public.admin_save_epay_settings` 并使用函数返回行,保留原子审计和脱敏响应。2026-07-30 新增前向迁移 `20260730010000_admin_payment_permissions.sql`,向 `admin_runtime` 最小授予套餐读写、订单只读、易支付配置读取及保存函数执行权限,并为启用 RLS 的支付表补齐角色策略;不授予订单写入或删除权限。 -- 验证:`frontend/tests/admin-contracts.test.ts` 锁定支付、套餐资源顺序;`frontend/tests/admin-payments-contract.test.ts` 锁定本地参数化 SQL、`identity.users` 联表、套餐 SQL CRUD/UUID/404、独立套餐页面、默认折叠和后台专用滚动容器;`frontend/tests/epay-settings.test.ts` 锁定 `chatEnabled` 回显、`queryAdminRows` 读取、参数化 `admin_save_epay_settings`、不依赖 Supabase builder/RPC、默认折叠和不泄露 key。2026-07-29 运行三份契约测试共 27 项全部通过;ESLint、TypeScript 与 `git diff --check` 结果记录在本次交付报告。2026-07-30 线上健康响应证明部署 SHA 为 `dd8e2ad9c7e76d0152b4563c43a45b1e26137035` 且本地业务库、身份库均健康;静态权限审计确认支付迁移缺少 `admin_runtime` grant/RLS。新增权限迁移契约后,支付、套餐、配置三组 21 项回归全部通过;生产态最终验证仍等待迁移应用和已登录 smoke,因此状态保持 `investigating`。 -- 防复发:self-hosted staging 后台查询不得依赖 LocalPostgresDataClient 未实现的 Supabase builder、RPC 或 Admin Auth 能力;支付与套餐必须保持独立资源顺序。套餐与易支付配置契约必须显式拒绝 Supabase builder/RPC 并锁定参数化 SQL、404、原子函数写入和安全错误响应;支付配置必须默认折叠,后台必须拥有独立滚动容器且不得放宽聊天的全局 `overflow:hidden`。易支付配置读写测试必须同时覆盖数据库列和公开字段;创建订单只生成经公网 SSRF 校验的签名收银台 URL,商户密钥只能参与服务端签名,不得进入 URL、响应、日志或审计。对话支付默认关闭,UI 与创建订单 API 必须共享服务端开关;可用性测试不得提交伪订单或返回 URL、PID、密钥、headers/body。 -- 相关记录:BUG-087、BUG-092 -- 复发自:BUG-093 -- 修复版本:待提交(本地可测) +- 修复版本:待提交 diff --git a/docs/adr/0001-separate-agent-failures-from-reply-ratings.md b/docs/adr/0001-separate-agent-failures-from-reply-ratings.md deleted file mode 100644 index e2f6f626..00000000 --- a/docs/adr/0001-separate-agent-failures-from-reply-ratings.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -status: accepted ---- - -# 分离 Agent 执行故障与回复评价 - -所有 Agent 会话使用稳定的会话、轮次、回复、执行尝试和请求标识建立关联,但将技术执行故障与用户对完整回复的质量评价建模为两类记录;服务端和客户端故障按请求标识关联去重,重复执行按轮次聚合展示。这样可以分别衡量系统可用性与回答质量,并保留重试轨迹,而不会把业务拒绝、技术失败和内容不满意混成同一种“报错”。 diff --git a/docs/adr/0002-minimize-and-expire-conversation-quality-content.md b/docs/adr/0002-minimize-and-expire-conversation-quality-content.md deleted file mode 100644 index fcf44d7f..00000000 --- a/docs/adr/0002-minimize-and-expire-conversation-quality-content.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -status: accepted ---- - -# 最小化并限期保留对话质量正文 - -对话质量记录按排错需要最小化收集:执行故障保存实际使用的上下文,负向评价保存对应 Agent 对话轮次,正向评价只保存脱敏统计元数据;禁止保存密钥、认证信息、系统提示词、原始第三方响应和完整堆栈。故障及负向评价正文最多保留 90 天,用户删除会话或撤回负向评价时提前清除相关正文;不可还原对话的聚合数据、审计轨迹和脱敏管理员备注可以长期保留,以平衡问题追踪与用户隐私。 diff --git a/docs/agents/domain.md b/docs/agents/domain.md deleted file mode 100644 index b548c538..00000000 --- a/docs/agents/domain.md +++ /dev/null @@ -1,51 +0,0 @@ -# Domain Docs - -How the engineering skills should consume this repo's domain documentation when exploring the codebase. - -## Before exploring, read these - -- **`CONTEXT.md`** at the repo root, or -- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic. -- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src//docs/adr/` for context-scoped decisions. - -If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved. - -## File structure - -Single-context repo (most repos): - -``` -/ -├── CONTEXT.md -├── docs/adr/ -│ ├── 0001-event-sourced-orders.md -│ └── 0002-postgres-for-write-model.md -└── src/ -``` - -Multi-context repo (presence of `CONTEXT-MAP.md` at the root): - -``` -/ -├── CONTEXT-MAP.md -├── docs/adr/ ← system-wide decisions -└── src/ - ├── ordering/ - │ ├── CONTEXT.md - │ └── docs/adr/ ← context-specific decisions - └── billing/ - ├── CONTEXT.md - └── docs/adr/ -``` - -## Use the glossary's vocabulary - -When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids. - -If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`). - -## Flag ADR conflicts - -If your output contradicts an existing ADR, surface it explicitly rather than silently overriding: - -> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_ diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md deleted file mode 100644 index 82cfbf5b..00000000 --- a/docs/agents/issue-tracker.md +++ /dev/null @@ -1,45 +0,0 @@ -# Issue tracker: GitHub - -Issues and PRDs for this repo live as GitHub issues. Use the `gh` CLI for all operations. - -## Conventions - -- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies. -- **Read an issue**: `gh issue view --comments`, filtering comments by `jq` and also fetching labels. -- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters. -- **Comment on an issue**: `gh issue comment --body "..."` -- **Apply / remove labels**: `gh issue edit --add-label "..."` / `--remove-label "..."` -- **Close**: `gh issue close --comment "..."` - -Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone. - -## Pull requests as a triage surface - -**PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_ - -When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents: - -- **Read a PR**: `gh pr view --comments` and `gh pr diff ` for the diff. -- **List external PRs for triage**: `gh pr list --state open --json number,title,body,labels,author,authorAssociation,comments` then keep only `authorAssociation` of `CONTRIBUTOR`, `FIRST_TIME_CONTRIBUTOR`, or `NONE` (drop `OWNER`/`MEMBER`/`COLLABORATOR`). -- **Comment / label / close**: `gh pr comment`, `gh pr edit --add-label`/`--remove-label`, `gh pr close`. - -GitHub shares one number space across issues and PRs, so a bare `#42` may be either — resolve with `gh pr view 42` and fall back to `gh issue view 42`. - -## When a skill says "publish to the issue tracker" - -Create a GitHub issue. - -## When a skill says "fetch the relevant ticket" - -Run `gh issue view --comments`. - -## Wayfinding operations - -Used by `/wayfinder`. The **map** is a single issue with **child** issues as tickets. - -- **Map**: a single issue labelled `wayfinder:map`, holding the Notes / Decisions-so-far / Fog body. `gh issue create --label wayfinder:map`. -- **Child ticket**: an issue linked to the map as a GitHub sub-issue (`gh api` on the sub-issues endpoint). Where sub-issues aren't enabled, add the child to a task list in the map body and put `Part of #` at the top of the child body. Labels: `wayfinder:` (`research`/`prototype`/`grilling`/`task`). Once claimed, the ticket is assigned to the driving dev. -- **Blocking**: GitHub's **native issue dependencies** — the canonical, UI-visible representation. Add an edge with `gh api --method POST repos///issues//dependencies/blocked_by -F issue_id=`, where `` is the blocker's numeric **database id** (`gh api repos///issues/ --jq .id`, _not_ the `#number` or `node_id`). GitHub reports `issue_dependencies_summary.blocked_by` (open blockers only — the live gate). Where dependencies aren't available, fall back to a `Blocked by: #, #` line at the top of the child body. A ticket is unblocked when every blocker is closed. -- **Frontier query**: list the map's open children (`gh issue list --state open`, scoped to the map's sub-issues / task list), drop any with an open blocker (`issue_dependencies_summary.blocked_by > 0`, or an open issue in the `Blocked by` line) or an assignee; first in map order wins. -- **Claim**: `gh issue edit --add-assignee @me` — the session's first write. -- **Resolve**: `gh issue comment --body ""`, then `gh issue close `, then append a context pointer (gist + link) to the map's Decisions-so-far. diff --git a/docs/agents/triage-labels.md b/docs/agents/triage-labels.md deleted file mode 100644 index b716855d..00000000 --- a/docs/agents/triage-labels.md +++ /dev/null @@ -1,15 +0,0 @@ -# Triage Labels - -The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker. - -| Label in mattpocock/skills | Label in our tracker | Meaning | -| -------------------------- | -------------------- | ---------------------------------------- | -| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue | -| `needs-info` | `needs-info` | Waiting on reporter for more information | -| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent | -| `ready-for-human` | `ready-for-human` | Requires human implementation | -| `wontfix` | `wontfix` | Will not be actioned | - -When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table. - -Edit the right-hand column to match whatever vocabulary you actually use. diff --git a/docs/operations/self-hosted-identity.md b/docs/operations/self-hosted-identity.md index 0b1e634d..2e7009cb 100644 --- a/docs/operations/self-hosted-identity.md +++ b/docs/operations/self-hosted-identity.md @@ -4,27 +4,29 @@ Staging uses Better Auth and the private local PostgreSQL cluster for both ident ## Staging mode -Keep these values exactly as shown: +Keep these two values exactly as shown: ```dotenv AUTH_PROVIDER=self-hosted SELF_HOSTED_IDENTITY_ENABLED=true -AUTH_USER_ORIGIN=https://staging.jyotisha.chat ``` -Staging has one browser identity surface on the main site. The same Better Auth user session serves ordinary pages and `/admin`; there is no independent admin origin, secret, cookie, or login host. Server routes translate that session into PostgreSQL request claims. Admin authorization then reads the persisted `identity.users.role` value and permits only `admin`; `viewer` and ordinary users receive `403`. The main auth route continues to return `404` for Better Auth `/api/auth/admin` plugin endpoints, and unknown hosts fail closed with `421`. +This makes both login hosts use isolated Better Auth surfaces. Public and admin sessions have different secrets and host-only cookie prefixes. Server routes translate the Better Auth session into PostgreSQL request claims and use the reviewed existing RLS/RPC business contract. The browser uses only same-origin APIs and does not need Supabase configuration. Use [the tracked staging identity example](../../deploy/.env.staging.identity.example) as a list of names only. Replace bracketed values directly on the server and keep `/opt/jyotisha-staging/.env.staging` owned by `deploy` with mode `0600`. -Generate `BETTER_AUTH_USER_SECRET` locally on the server: +Generate separate secrets locally on the server: ```bash openssl rand -base64 32 +openssl rand -base64 32 ``` -Do not reuse it as a PostgreSQL password. `IDENTITY_DATABASE_URL`, `APP_DATABASE_URL`, and `ADMIN_DATABASE_URL` use their matching passwords from `.env.staging.database`, percent-encoded only in each URL password component. All three must point to the private Compose hostname `postgres:5432/jyotisha`; never publish PostgreSQL on a host port. +Do not reuse either value as a PostgreSQL password. `IDENTITY_DATABASE_URL`, `APP_DATABASE_URL`, and `ADMIN_DATABASE_URL` use their matching passwords from `.env.staging.database`, percent-encoded only in each URL password component. All three must point to the private Compose hostname `postgres:5432/jyotisha`; never publish PostgreSQL on a host port. -The Resend key must be staging-only. `RESEND_FROM_EMAIL` must use a sender/domain verified in Resend. CI never receives this key and uses an in-memory sender. `ADMIN_EMAILS` remains relevant only to the legacy Supabase production path; it is not self-hosted admin authorization. +The Resend key must be staging-only. `RESEND_FROM_EMAIL` must use a sender/domain verified in Resend. CI never receives this key and uses an in-memory sender. + +Set `ADMIN_EMAILS` to the staging administrator allowlist. Generate an independent `JYOTISH_DYNAMIC_RECTIFICATION_TOKEN` and place the same value in the shared application env consumed by the web and private API containers; do not reuse a database or Better Auth secret. Validate without printing values: @@ -36,17 +38,17 @@ bash deploy/validate-staging-env.sh .env.staging ## Migration and smoke checks -Apply the reviewed PostgreSQL migrations through the existing `Migrate Staging Database` workflow before deploying the web image. Better Auth users are transactionally projected into `auth.users`, which creates their business profile through the existing trigger. +Apply the reviewed PostgreSQL migrations through the existing `Migrate Staging Database` workflow before deploying the web image. The workflow first ensures the compatibility roles exist, then applies the identity schema, the local `auth` compatibility layer, and all reviewed business migrations under the migration ledger. Better Auth users are transactionally projected into `auth.users`, which creates their business profile through the existing trigger. -After deployment, verify the single-domain contract: +After deployment: ```bash -curl -fsS https://staging.jyotisha.chat/login >/dev/null -test "$(curl -sS -o /dev/null -w '%{http_code}' https://staging.jyotisha.chat/api/admin/session)" = 401 -test "$(curl -sS -o /dev/null -w '%{http_code}' https://staging.jyotisha.chat/api/account)" = 401 +curl -fsS https://admin.staging.jyotisha.chat/login >/dev/null +curl -fsS https://admin.staging.jyotisha.chat/api/auth/get-session +test "$(curl -sS -o /dev/null -w '%{http_code}' https://staging.jyotisha.chat/admin/codes)" = 404 ``` -An anonymous `/admin` request redirects to `/login`. An authenticated non-admin, including a persisted `viewer`, must not render the admin layout and every `/api/admin/*` route must independently return `403`. Promote a staging user only through a reviewed database operation; the persisted role must include `admin` before the main-site session can enter the backend. +The admin root redirects to `/admin/codes`; the public host rejects `/admin` and `/api/admin` paths. An unknown or unpromoted email cannot create an admin session. Promote an imported staging user only through a reviewed database/admin operation; the persisted `identity.users.role` value must include `admin` before the admin OTP flow can issue a cookie. ## Optional import rehearsal @@ -73,8 +75,8 @@ Reruns are idempotent by UUID and the whole import is transactional. Duplicate c ## Rollback and rotation -An application rollback must use a previously validated staging image and does not reverse database migrations. Existing self-hosted sessions and data remain in PostgreSQL; do not delete identity or business rows during application rollback. Returning staging to Supabase requires a separate reviewed data-reconciliation and provider-switch change. +An application rollback must use a previously validated staging image and does not reverse database migrations. Existing self-hosted sessions and data remain in PostgreSQL; do not delete identity or business rows during application rollback. Returning staging to Supabase would require a separate reviewed data-reconciliation and provider-switch change, not an environment-only toggle. -Rotating `BETTER_AUTH_USER_SECRET` invalidates all self-hosted browser sessions, including admins. Restart the web service and verify login, anonymous admin API rejection, admin access, and viewer rejection. Rotate a leaked Resend key in Resend first, replace the server value, then restart. Never print old or new values. +Rotating either Better Auth secret invalidates only that surface's existing sessions. Rotate user and admin secrets separately, restart the web service, and verify the corresponding host. Rotate a leaked Resend key in Resend first, replace the server value, then restart. Never print the old or new values. Production `AUTH_PROVIDER=self-hosted` remains blocked until data reconciliation passes, production backups and restore drills exist, operational monitoring is ready, and a separate reviewed production cutover plan is approved. diff --git a/docs/superpowers/plans/2026-07-21-self-hosted-identity.md b/docs/superpowers/plans/2026-07-21-self-hosted-identity.md index 53c61c26..aaa37b31 100644 --- a/docs/superpowers/plans/2026-07-21-self-hosted-identity.md +++ b/docs/superpowers/plans/2026-07-21-self-hosted-identity.md @@ -1,5 +1,3 @@ -> Superseded 2026-07-29: staging browser identity and admin access now use one main-site Better Auth user session; the dual-domain admin surface in this historical plan is inactive. - # Self-Hosted Identity Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. diff --git a/docs/superpowers/specs/2026-07-20-supabase-exit-backend-design.md b/docs/superpowers/specs/2026-07-20-supabase-exit-backend-design.md index cb86551c..1484b232 100644 --- a/docs/superpowers/specs/2026-07-20-supabase-exit-backend-design.md +++ b/docs/superpowers/specs/2026-07-20-supabase-exit-backend-design.md @@ -1,5 +1,3 @@ -> Superseded 2026-07-29: staging browser identity and admin access now use one main-site Better Auth user session; the dual-domain admin surface in this historical specification is inactive. - # Jyotisha Supabase Exit and Self-Hosted Backend Design Date: 2026-07-20 diff --git a/frontend/db/migrations/20260727000000_admin_viewer_identity.sql b/frontend/db/migrations/20260727000000_admin_viewer_identity.sql deleted file mode 100644 index 9fef5a39..00000000 --- a/frontend/db/migrations/20260727000000_admin_viewer_identity.sql +++ /dev/null @@ -1,6 +0,0 @@ --- Admin-host sessions may be created for read-only viewers. API authorization --- remains server-side and is resolved from this persisted role on every request. --- Existing identity migrations already grant admin_runtime these reads; repeat the --- least-privilege user grant so drifted staging databases fail closed at login. - -grant select on table identity.users to admin_runtime; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index da1047c3..acb1727f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,19 +28,13 @@ "react-dom": "19.2.4", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", - "server-only": "^0.0.1", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.2", "thinking-orbs": "^0.1.1", "tsx": "^4.23.1", "tw-animate-css": "^1.4.0", - "zod": "^3.25.76", - "@ant-design/icons": "^6.3.2", - "@refinedev/antd": "^6.0.3", - "@refinedev/core": "^5.0.12", - "@refinedev/nextjs-router": "^7.0.5", - "antd": "^5.29.3" + "zod": "^3.25.76" }, "devDependencies": { "@types/node": "^20", @@ -10465,12 +10459,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/server-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", - "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", - "license": "MIT" - }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -11703,2358 +11691,6 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } - }, - "node_modules/@ant-design/icons": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.3.2.tgz", - "integrity": "sha512-B6O5a5XJ4wjtNOfZejXYwHW5zvKV5gYkjGf11dHGLEbKn0ABDGndo41+gfIiXyTFhvESj4XTotuud33mUFid0g==", - "license": "MIT", - "dependencies": { - "@ant-design/colors": "^8.0.1", - "@ant-design/icons-svg": "^4.5.0", - "@rc-component/util": "^1.11.0", - "clsx": "^2.1.1" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/@refinedev/antd": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@refinedev/antd/-/antd-6.0.3.tgz", - "integrity": "sha512-adNxHZJuca3TN4y1zXpamspqc0yi5hAqlzveTKAXLDuZ8C546xPdihmpZ/bWGG1fjYzWKLctb3YIoUw6d/WDgA==", - "license": "MIT", - "dependencies": { - "@ant-design/icons": "^5.5.1", - "@ant-design/pro-layout": "^7.21.1", - "@refinedev/ui-types": "^2.0.1", - "@tanstack/react-query": "^5.81.5", - "antd": "^5.23.0", - "dayjs": "^1.10.7", - "react-markdown": "^6.0.1", - "remark-gfm": "^1.0.0", - "sunflower-antd": "1.0.0-beta.3", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@refinedev/core": "^5.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "antd": "^5.23.0", - "dayjs": "^1.10.7", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@refinedev/core": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/@refinedev/core/-/core-5.0.12.tgz", - "integrity": "sha512-9y5Bi9Lb7XyJmM55b8rCeBTDRCBU41p47OymJldasLfrtpUm2EwI+27DjjNpHTOugymiZsIbLlPtHCPQIXBHcg==", - "license": "MIT", - "dependencies": { - "@refinedev/devtools-internal": "2.0.2", - "@tanstack/react-query": "^5.81.5", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "papaparse": "^5.3.0", - "pluralize": "^8.0.0", - "qs": "^6.10.1", - "tslib": "^2.6.2", - "warn-once": "^0.1.0" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@tanstack/react-query": "^5.81.5", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@refinedev/nextjs-router": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/@refinedev/nextjs-router/-/nextjs-router-7.0.5.tgz", - "integrity": "sha512-Z724KBsnEtESGYZMntXEhXr9gmQD/kD6s7poeMY4HeLtWLfNyJPdopHntD4BYMU1ApZweDBJeSqEuWjoL3/x5A==", - "license": "MIT", - "dependencies": { - "qs": "^6.10.1", - "warn-once": "^0.1.0" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@refinedev/core": "^5.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "next": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/antd": { - "version": "5.29.3", - "resolved": "https://registry.npmjs.org/antd/-/antd-5.29.3.tgz", - "integrity": "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A==", - "license": "MIT", - "dependencies": { - "@ant-design/colors": "^7.2.1", - "@ant-design/cssinjs": "^1.23.0", - "@ant-design/cssinjs-utils": "^1.1.3", - "@ant-design/fast-color": "^2.0.6", - "@ant-design/icons": "^5.6.1", - "@ant-design/react-slick": "~1.1.2", - "@babel/runtime": "^7.26.0", - "@rc-component/color-picker": "~2.0.1", - "@rc-component/mutate-observer": "^1.1.0", - "@rc-component/qrcode": "~1.1.0", - "@rc-component/tour": "~1.15.1", - "@rc-component/trigger": "^2.3.0", - "classnames": "^2.5.1", - "copy-to-clipboard": "^3.3.3", - "dayjs": "^1.11.11", - "rc-cascader": "~3.34.0", - "rc-checkbox": "~3.5.0", - "rc-collapse": "~3.9.0", - "rc-dialog": "~9.6.0", - "rc-drawer": "~7.3.0", - "rc-dropdown": "~4.2.1", - "rc-field-form": "~2.7.1", - "rc-image": "~7.12.0", - "rc-input": "~1.8.0", - "rc-input-number": "~9.5.0", - "rc-mentions": "~2.20.0", - "rc-menu": "~9.16.1", - "rc-motion": "^2.9.5", - "rc-notification": "~5.6.4", - "rc-pagination": "~5.1.0", - "rc-picker": "~4.11.3", - "rc-progress": "~4.0.0", - "rc-rate": "~2.13.1", - "rc-resize-observer": "^1.4.3", - "rc-segmented": "~2.7.0", - "rc-select": "~14.16.8", - "rc-slider": "~11.1.9", - "rc-steps": "~6.0.1", - "rc-switch": "~4.1.0", - "rc-table": "~7.54.0", - "rc-tabs": "~15.7.0", - "rc-textarea": "~1.10.2", - "rc-tooltip": "~6.4.0", - "rc-tree": "~5.13.1", - "rc-tree-select": "~5.27.0", - "rc-upload": "~4.11.0", - "rc-util": "^5.44.4", - "scroll-into-view-if-needed": "^3.1.0", - "throttle-debounce": "^5.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ant-design" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@ant-design/colors": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.1.tgz", - "integrity": "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==", - "license": "MIT", - "dependencies": { - "@ant-design/fast-color": "^3.0.0" - } - }, - "node_modules/@ant-design/icons-svg": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.5.0.tgz", - "integrity": "sha512-1BTUFyKPTBZ53MuTP8s0k5SFEXL7o3VHEOwLgzaoWKwnBeqIcqUtVshc4SKzhI6uACfqhJqBwBUE9FsWR3uULA==", - "license": "MIT" - }, - "node_modules/@rc-component/util": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.12.0.tgz", - "integrity": "sha512-AEjPL8JVdohIITaiXokyjL9WQ6tKWWjAYK9QU16tGNE9JaQABBQy+hA4H2Lup5MgXy9yY3iLrbZJheuU13hTdQ==", - "license": "MIT", - "dependencies": { - "is-mobile": "^5.0.0", - "react-is": "^19.2.7" - }, - "peerDependencies": { - "react": ">=18.0.0", - "react-dom": ">=18.0.0" - } - }, - "node_modules/@refinedev/antd/node_modules/@ant-design/icons": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", - "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", - "license": "MIT", - "dependencies": { - "@ant-design/colors": "^7.0.0", - "@ant-design/icons-svg": "^4.4.0", - "@babel/runtime": "^7.24.8", - "classnames": "^2.2.6", - "rc-util": "^5.31.1" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/@ant-design/pro-layout": { - "version": "7.22.7", - "resolved": "https://registry.npmjs.org/@ant-design/pro-layout/-/pro-layout-7.22.7.tgz", - "integrity": "sha512-fvmtNA1r9SaasVIQIQt611VSlNxtVxDbQ3e+1GhYQza3tVJi/3gCZuDyfMfTnbLmf3PaW/YvLkn7MqDbzAzoLA==", - "license": "MIT", - "dependencies": { - "@ant-design/cssinjs": "^1.21.1", - "@ant-design/icons": "^5.0.0", - "@ant-design/pro-provider": "2.16.2", - "@ant-design/pro-utils": "2.18.0", - "@babel/runtime": "^7.18.0", - "@umijs/route-utils": "^4.0.0", - "@umijs/use-params": "^1.0.9", - "classnames": "^2.3.2", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "path-to-regexp": "8.2.0", - "rc-resize-observer": "^1.1.0", - "rc-util": "^5.0.6", - "swr": "^2.0.0", - "warning": "^4.0.3" - }, - "peerDependencies": { - "antd": "^4.24.15 || ^5.11.2", - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@refinedev/ui-types": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@refinedev/ui-types/-/ui-types-2.0.1.tgz", - "integrity": "sha512-Fxsgr2JEsyEVGr5rMvOasQP5tj/1yD2m4M9XqDZQ+65B/ZB/vbkbB5+ltAhNlX2UwX8jr1mo8fnutHBltYxwfA==", - "license": "MIT", - "dependencies": { - "@refinedev/core": "^5.0.5", - "dayjs": "^1.10.7", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@refinedev/core": "^5.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.101.4", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.4.tgz", - "integrity": "sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.101.4" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, - "node_modules/dayjs": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", - "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", - "license": "MIT" - }, - "node_modules/@refinedev/antd/node_modules/react-markdown": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-6.0.3.tgz", - "integrity": "sha512-kQbpWiMoBHnj9myLlmZG9T1JdoT/OEyHK7hqM6CqFT14MAkgWiWBUYijLyBmxbntaN6dCDicPcUhWhci1QYodg==", - "license": "MIT", - "dependencies": { - "@types/hast": "^2.0.0", - "@types/unist": "^2.0.3", - "comma-separated-tokens": "^1.0.0", - "prop-types": "^15.7.2", - "property-information": "^5.3.0", - "react-is": "^17.0.0", - "remark-parse": "^9.0.0", - "remark-rehype": "^8.0.0", - "space-separated-tokens": "^1.1.0", - "style-to-object": "^0.3.0", - "unified": "^9.0.0", - "unist-util-visit": "^2.0.0", - "vfile": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" - } - }, - "node_modules/@refinedev/antd/node_modules/remark-gfm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-1.0.0.tgz", - "integrity": "sha512-KfexHJCiqvrdBZVbQ6RopMZGwaXz6wFJEfByIuEwGf0arvITHjiKKZ1dpXujjH9KZdm1//XJQwgfnJ3lmXaDPA==", - "license": "MIT", - "dependencies": { - "mdast-util-gfm": "^0.1.0", - "micromark-extension-gfm": "^0.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/sunflower-antd": { - "version": "1.0.0-beta.3", - "resolved": "https://registry.npmjs.org/sunflower-antd/-/sunflower-antd-1.0.0-beta.3.tgz", - "integrity": "sha512-SAdjHgNemTFNxUF/QJ2KdC0x6wWpY1EsMJMo+F5KIHCDRsUUahjAIldoK+ejH00rPgUoCOhAHQ/ob/J7eyZ5qg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@refinedev/devtools-internal": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@refinedev/devtools-internal/-/devtools-internal-2.0.2.tgz", - "integrity": "sha512-1YYizOW1lyy9ep8eQ7TcUPBooKXIlvzTLjLdDArsQwx7P33cn2uXdqM7So5VhlNFXhjOjAKFgrH5c1jleRF8Jg==", - "license": "MIT", - "dependencies": { - "@refinedev/devtools-shared": "2.0.2", - "@tanstack/react-query": "^5.81.5", - "error-stack-parser": "^2.1.4" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", - "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", - "license": "MIT" - }, - "node_modules/papaparse": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.4.tgz", - "integrity": "sha512-SwzWD9gl/ElwYLCI0nUja1mFJzjq2D8ziShfNBa7zCHzkOozeOGDwHWQ+tvCzEZcewecWZ5U7kUopDnG+DFYEQ==", - "license": "MIT" - }, - "node_modules/pluralize": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", - "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/warn-once": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz", - "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", - "license": "MIT" - }, - "node_modules/antd/node_modules/@ant-design/colors": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", - "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", - "license": "MIT", - "dependencies": { - "@ant-design/fast-color": "^2.0.6" - } - }, - "node_modules/@ant-design/cssinjs": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz", - "integrity": "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.11.1", - "@emotion/hash": "^0.8.0", - "@emotion/unitless": "^0.7.5", - "classnames": "^2.3.1", - "csstype": "^3.1.3", - "rc-util": "^5.35.0", - "stylis": "^4.3.4" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/@ant-design/cssinjs-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz", - "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==", - "license": "MIT", - "dependencies": { - "@ant-design/cssinjs": "^1.21.0", - "@babel/runtime": "^7.23.2", - "rc-util": "^5.38.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/antd/node_modules/@ant-design/fast-color": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", - "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.24.7" - }, - "engines": { - "node": ">=8.x" - } - }, - "node_modules/antd/node_modules/@ant-design/icons": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", - "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", - "license": "MIT", - "dependencies": { - "@ant-design/colors": "^7.0.0", - "@ant-design/icons-svg": "^4.4.0", - "@babel/runtime": "^7.24.8", - "classnames": "^2.2.6", - "rc-util": "^5.31.1" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/@ant-design/react-slick": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", - "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.4", - "classnames": "^2.2.5", - "json2mq": "^0.2.0", - "resize-observer-polyfill": "^1.5.1", - "throttle-debounce": "^5.0.0" - }, - "peerDependencies": { - "react": ">=16.9.0" - } - }, - "node_modules/@rc-component/color-picker": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz", - "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==", - "license": "MIT", - "dependencies": { - "@ant-design/fast-color": "^2.0.6", - "@babel/runtime": "^7.23.6", - "classnames": "^2.2.6", - "rc-util": "^5.38.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/mutate-observer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", - "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.0", - "classnames": "^2.3.2", - "rc-util": "^5.24.4" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/qrcode": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.3.tgz", - "integrity": "sha512-aGv6alnn4HbDEsURzKP+jv13rbi1VxmAYfBNZr5GKF1iohMNWy5tAVoJ1E3cOvzMB1kbUPvCXchM6zSFlRGPhA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.24.7" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/tour": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz", - "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.0", - "@rc-component/portal": "^1.0.0-9", - "@rc-component/trigger": "^2.0.0", - "classnames": "^2.3.2", - "rc-util": "^5.24.4" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/trigger": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.3.1.tgz", - "integrity": "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.2", - "@rc-component/portal": "^1.1.0", - "classnames": "^2.3.2", - "rc-motion": "^2.0.0", - "rc-resize-observer": "^1.3.1", - "rc-util": "^5.44.0" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "license": "MIT" - }, - "node_modules/copy-to-clipboard": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", - "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", - "license": "MIT", - "dependencies": { - "toggle-selection": "^1.0.6" - } - }, - "node_modules/rc-cascader": { - "version": "3.34.0", - "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz", - "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.25.7", - "classnames": "^2.3.1", - "rc-select": "~14.16.2", - "rc-tree": "~5.13.0", - "rc-util": "^5.43.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-checkbox": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz", - "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "^2.3.2", - "rc-util": "^5.25.2" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-collapse": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz", - "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "2.x", - "rc-motion": "^2.3.4", - "rc-util": "^5.27.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-dialog": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz", - "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/portal": "^1.0.0-8", - "classnames": "^2.2.6", - "rc-motion": "^2.3.0", - "rc-util": "^5.21.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-drawer": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz", - "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@rc-component/portal": "^1.1.1", - "classnames": "^2.2.6", - "rc-motion": "^2.6.1", - "rc-util": "^5.38.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-dropdown": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz", - "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@rc-component/trigger": "^2.0.0", - "classnames": "^2.2.6", - "rc-util": "^5.44.1" - }, - "peerDependencies": { - "react": ">=16.11.0", - "react-dom": ">=16.11.0" - } - }, - "node_modules/rc-field-form": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.1.tgz", - "integrity": "sha512-vKeSifSJ6HoLaAB+B8aq/Qgm8a3dyxROzCtKNCsBQgiverpc4kWDQihoUwzUj+zNWJOykwSY4dNX3QrGwtVb9A==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.0", - "@rc-component/async-validator": "^5.0.3", - "rc-util": "^5.32.2" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-image": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz", - "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.11.2", - "@rc-component/portal": "^1.0.2", - "classnames": "^2.2.6", - "rc-dialog": "~9.6.0", - "rc-motion": "^2.6.2", - "rc-util": "^5.34.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-input": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", - "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.11.1", - "classnames": "^2.2.1", - "rc-util": "^5.18.1" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/rc-input-number": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz", - "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/mini-decimal": "^1.0.1", - "classnames": "^2.2.5", - "rc-input": "~1.8.0", - "rc-util": "^5.40.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-mentions": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz", - "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.22.5", - "@rc-component/trigger": "^2.0.0", - "classnames": "^2.2.6", - "rc-input": "~1.8.0", - "rc-menu": "~9.16.0", - "rc-textarea": "~1.10.0", - "rc-util": "^5.34.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-menu": { - "version": "9.16.1", - "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz", - "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/trigger": "^2.0.0", - "classnames": "2.x", - "rc-motion": "^2.4.3", - "rc-overflow": "^1.3.1", - "rc-util": "^5.27.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-motion": { - "version": "2.9.5", - "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", - "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.11.1", - "classnames": "^2.2.1", - "rc-util": "^5.44.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-notification": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz", - "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "2.x", - "rc-motion": "^2.9.0", - "rc-util": "^5.20.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-pagination": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz", - "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "^2.3.2", - "rc-util": "^5.38.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-picker": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz", - "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.24.7", - "@rc-component/trigger": "^2.0.0", - "classnames": "^2.2.1", - "rc-overflow": "^1.3.2", - "rc-resize-observer": "^1.4.0", - "rc-util": "^5.43.0" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "date-fns": ">= 2.x", - "dayjs": ">= 1.x", - "luxon": ">= 3.x", - "moment": ">= 2.x", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - }, - "peerDependenciesMeta": { - "date-fns": { - "optional": true - }, - "dayjs": { - "optional": true - }, - "luxon": { - "optional": true - }, - "moment": { - "optional": true - } - } - }, - "node_modules/rc-progress": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", - "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "^2.2.6", - "rc-util": "^5.16.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-rate": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz", - "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "^2.2.5", - "rc-util": "^5.0.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-resize-observer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", - "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.20.7", - "classnames": "^2.2.1", - "rc-util": "^5.44.1", - "resize-observer-polyfill": "^1.5.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-segmented": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.1.tgz", - "integrity": "sha512-izj1Nw/Dw2Vb7EVr+D/E9lUTkBe+kKC+SAFSU9zqr7WV2W5Ktaa9Gc7cB2jTqgk8GROJayltaec+DBlYKc6d+g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.11.1", - "classnames": "^2.2.1", - "rc-motion": "^2.4.4", - "rc-util": "^5.17.0" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/rc-select": { - "version": "14.16.8", - "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz", - "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/trigger": "^2.1.1", - "classnames": "2.x", - "rc-motion": "^2.0.1", - "rc-overflow": "^1.3.1", - "rc-util": "^5.16.1", - "rc-virtual-list": "^3.5.2" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/rc-slider": { - "version": "11.1.9", - "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.9.tgz", - "integrity": "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "^2.2.5", - "rc-util": "^5.36.0" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-steps": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", - "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.16.7", - "classnames": "^2.2.3", - "rc-util": "^5.16.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-switch": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", - "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.21.0", - "classnames": "^2.2.1", - "rc-util": "^5.30.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-table": { - "version": "7.54.0", - "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.54.0.tgz", - "integrity": "sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/context": "^1.4.0", - "classnames": "^2.2.5", - "rc-resize-observer": "^1.1.0", - "rc-util": "^5.44.3", - "rc-virtual-list": "^3.14.2" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-tabs": { - "version": "15.7.0", - "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.7.0.tgz", - "integrity": "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.11.2", - "classnames": "2.x", - "rc-dropdown": "~4.2.0", - "rc-menu": "~9.16.0", - "rc-motion": "^2.6.2", - "rc-resize-observer": "^1.0.0", - "rc-util": "^5.34.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-textarea": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.2.tgz", - "integrity": "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "^2.2.1", - "rc-input": "~1.8.0", - "rc-resize-observer": "^1.0.0", - "rc-util": "^5.27.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-tooltip": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz", - "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.11.2", - "@rc-component/trigger": "^2.0.0", - "classnames": "^2.3.1", - "rc-util": "^5.44.3" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-tree": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz", - "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "2.x", - "rc-motion": "^2.0.1", - "rc-util": "^5.16.1", - "rc-virtual-list": "^3.5.1" - }, - "engines": { - "node": ">=10.x" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/rc-tree-select": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz", - "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.25.7", - "classnames": "2.x", - "rc-select": "~14.16.2", - "rc-tree": "~5.13.0", - "rc-util": "^5.43.0" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/rc-upload": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.11.0.tgz", - "integrity": "sha512-ZUyT//2JAehfHzjWowqROcwYJKnZkIUGWaTE/VogVrepSl7AFNbQf4+zGfX4zl9Vrj/Jm8scLO0R6UlPDKK4wA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "classnames": "^2.2.5", - "rc-util": "^5.2.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-util": { - "version": "5.44.4", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", - "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "react-is": "^18.2.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/scroll-into-view-if-needed": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", - "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", - "license": "MIT", - "dependencies": { - "compute-scroll-into-view": "^3.0.2" - } - }, - "node_modules/throttle-debounce": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", - "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", - "license": "MIT", - "engines": { - "node": ">=12.22" - } - }, - "node_modules/@ant-design/fast-color": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.1.tgz", - "integrity": "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==", - "license": "MIT", - "engines": { - "node": ">=8.x" - } - }, - "node_modules/is-mobile": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", - "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", - "license": "MIT" - }, - "node_modules/@rc-component/util/node_modules/react-is": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", - "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", - "license": "MIT" - }, - "node_modules/@refinedev/antd/node_modules/@ant-design/colors": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", - "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", - "license": "MIT", - "dependencies": { - "@ant-design/fast-color": "^2.0.6" - } - }, - "node_modules/@ant-design/pro-layout/node_modules/@ant-design/icons": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", - "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", - "license": "MIT", - "dependencies": { - "@ant-design/colors": "^7.0.0", - "@ant-design/icons-svg": "^4.4.0", - "@babel/runtime": "^7.24.8", - "classnames": "^2.2.6", - "rc-util": "^5.31.1" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/@ant-design/pro-provider": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/@ant-design/pro-provider/-/pro-provider-2.16.2.tgz", - "integrity": "sha512-0KmCH1EaOND787Jz6VRMYtLNZmqfT0JPjdUfxhyOxFfnBRfrjyfZgIa6CQoAJLEUMWv57PccWS8wRHVUUk2Yiw==", - "license": "MIT", - "dependencies": { - "@ant-design/cssinjs": "^1.21.1", - "@babel/runtime": "^7.18.0", - "@ctrl/tinycolor": "^3.4.0", - "dayjs": "^1.11.10", - "rc-util": "^5.0.1", - "swr": "^2.0.0" - }, - "peerDependencies": { - "antd": "^4.24.15 || ^5.11.2", - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@ant-design/pro-utils": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-2.18.0.tgz", - "integrity": "sha512-8+ikyrN8L8a8Ph4oeHTOJEiranTj18+9+WHCHjKNdEfukI7Rjn8xpYdLJWb2AUJkb9d4eoAqjd5+k+7w81Df0w==", - "license": "MIT", - "dependencies": { - "@ant-design/icons": "^5.0.0", - "@ant-design/pro-provider": "2.16.2", - "@babel/runtime": "^7.18.0", - "classnames": "^2.3.2", - "dayjs": "^1.11.10", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", - "rc-util": "^5.0.6", - "safe-stable-stringify": "^2.4.3", - "swr": "^2.0.0" - }, - "peerDependencies": { - "antd": "^4.24.15 || ^5.11.2", - "react": ">=17.0.0", - "react-dom": ">=17.0.0" - } - }, - "node_modules/@umijs/route-utils": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@umijs/route-utils/-/route-utils-4.0.3.tgz", - "integrity": "sha512-zPEcYhl1cSfkSRDzzGgoD1mDvGjxoOTJFvkn55srfgdQ3NZe2ZMCScCU6DEnOxuKP1XDVf8pqyqCDVd2+RCQIw==", - "license": "MIT" - }, - "node_modules/@umijs/use-params": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@umijs/use-params/-/use-params-1.0.9.tgz", - "integrity": "sha512-QlN0RJSBVQBwLRNxbxjQ5qzqYIGn+K7USppMoIOVlf7fxXHsnQZ2bEsa6Pm74bt6DVQxpUE8HqvdStn6Y9FV1w==", - "license": "MIT", - "peerDependencies": { - "react": "*" - } - }, - "node_modules/@ant-design/pro-layout/node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/swr": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.2.tgz", - "integrity": "sha512-ej644Y2bvkIajfR32KGeSSdBXQW+ScjGjkybZgSE7kFpk9eGnV44XY9FJylXi+W75pavSX1PVNB57W5EbhGIYw==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "node_modules/@tanstack/query-core": { - "version": "5.101.4", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", - "integrity": "sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@refinedev/antd/node_modules/@types/hast": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/@refinedev/antd/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/@refinedev/antd/node_modules/comma-separated-tokens": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", - "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/@refinedev/antd/node_modules/property-information": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", - "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/@refinedev/antd/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "license": "MIT" - }, - "node_modules/@refinedev/antd/node_modules/remark-parse": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", - "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^0.8.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/remark-rehype": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-8.1.0.tgz", - "integrity": "sha512-EbCu9kHgAxKmW1yEYjx3QafMyGY3q8noUbNUI5xyKbaFP89wbhDrKxyIQNukNYthzjNHZu6J7hwFg7hRm1svYA==", - "license": "MIT", - "dependencies": { - "mdast-util-to-hast": "^10.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/space-separated-tokens": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", - "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/@refinedev/antd/node_modules/style-to-object": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", - "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==", - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.1.1" - } - }, - "node_modules/@refinedev/antd/node_modules/unified": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", - "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", - "license": "MIT", - "dependencies": { - "bail": "^1.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^2.0.0", - "trough": "^1.0.0", - "vfile": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/unist-util-visit": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", - "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0", - "unist-util-visit-parents": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/vfile": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", - "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^2.0.0", - "vfile-message": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/mdast-util-gfm": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-0.1.2.tgz", - "integrity": "sha512-NNkhDx/qYcuOWB7xHUGWZYVXvjPFFd6afg6/e2g+SV4r9q5XUcCbV4Wfa3DLYIiD+xAEZc6K4MGaE/m0KDcPwQ==", - "license": "MIT", - "dependencies": { - "mdast-util-gfm-autolink-literal": "^0.1.0", - "mdast-util-gfm-strikethrough": "^0.2.0", - "mdast-util-gfm-table": "^0.1.0", - "mdast-util-gfm-task-list-item": "^0.1.0", - "mdast-util-to-markdown": "^0.6.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/micromark-extension-gfm": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-0.3.3.tgz", - "integrity": "sha512-oVN4zv5/tAIA+l3GbMi7lWeYpJ14oQyJ3uEim20ktYFAcfX1x3LNlFGGlmrZHt7u9YlKExmyJdDGaTt6cMSR/A==", - "license": "MIT", - "dependencies": { - "micromark": "~2.11.0", - "micromark-extension-gfm-autolink-literal": "~0.5.0", - "micromark-extension-gfm-strikethrough": "~0.6.5", - "micromark-extension-gfm-table": "~0.4.0", - "micromark-extension-gfm-tagfilter": "~0.3.0", - "micromark-extension-gfm-task-list-item": "~0.3.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/devtools-shared": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@refinedev/devtools-shared/-/devtools-shared-2.0.2.tgz", - "integrity": "sha512-3cTjR1mEWn0tHFZBfPD5aVpBGLUhpAkfjqYCwKrijIicr1Utp/j0BqiPRnNqTf+W71HTng3znBpUhnR83u+tuA==", - "license": "MIT", - "dependencies": { - "@tanstack/react-query": "^5.81.5", - "error-stack-parser": "^2.1.4" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/error-stack-parser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", - "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", - "license": "MIT", - "dependencies": { - "stackframe": "^1.3.4" - } - }, - "node_modules/@emotion/hash": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", - "license": "MIT" - }, - "node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", - "license": "MIT" - }, - "node_modules/stylis": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", - "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", - "license": "MIT" - }, - "node_modules/json2mq": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", - "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", - "license": "MIT", - "dependencies": { - "string-convert": "^0.2.0" - } - }, - "node_modules/resize-observer-polyfill": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "license": "MIT" - }, - "node_modules/@rc-component/color-picker/node_modules/@ant-design/fast-color": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", - "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.24.7" - }, - "engines": { - "node": ">=8.x" - } - }, - "node_modules/@rc-component/portal": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", - "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.0", - "classnames": "^2.3.2", - "rc-util": "^5.24.4" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/toggle-selection": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", - "license": "MIT" - }, - "node_modules/@rc-component/async-validator": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.2.tgz", - "integrity": "sha512-WYbrZSjzznU1ekD0qFq2qRxt309VoS61MTG5npnFQlKYcoy9IzU8T+ZCIhq5bGAXRbXysABFWTspicMfmWFwow==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.24.4" - }, - "engines": { - "node": ">=14.x" - } - }, - "node_modules/@rc-component/mini-decimal": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.4.tgz", - "integrity": "sha512-xiuXcaCwyOWpD8a8scdExFl+bntNphAW8XeenL1ig2en0AAZY0Pcp4pC0dI22qJ+NvxKn9RoNIoRdqYU3BLH4w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.0" - }, - "engines": { - "node": ">=8.x" - } - }, - "node_modules/rc-overflow": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.5.0.tgz", - "integrity": "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.11.1", - "classnames": "^2.2.1", - "rc-resize-observer": "^1.0.0", - "rc-util": "^5.37.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-virtual-list": { - "version": "3.19.2", - "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz", - "integrity": "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.20.0", - "classnames": "^2.2.6", - "rc-resize-observer": "^1.0.0", - "rc-util": "^5.36.0" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/context": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", - "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.1", - "rc-util": "^5.27.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-util/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" - }, - "node_modules/compute-scroll-into-view": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", - "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", - "license": "MIT" - }, - "node_modules/@refinedev/antd/node_modules/@ant-design/fast-color": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", - "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.24.7" - }, - "engines": { - "node": ">=8.x" - } - }, - "node_modules/@ant-design/pro-layout/node_modules/@ant-design/colors": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", - "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", - "license": "MIT", - "dependencies": { - "@ant-design/fast-color": "^2.0.6" - } - }, - "node_modules/@ctrl/tinycolor": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", - "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/@ant-design/pro-utils/node_modules/@ant-design/icons": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", - "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", - "license": "MIT", - "dependencies": { - "@ant-design/colors": "^7.0.0", - "@ant-design/icons-svg": "^4.4.0", - "@babel/runtime": "^7.24.8", - "classnames": "^2.2.6", - "rc-util": "^5.31.1" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/@refinedev/antd/node_modules/mdast-util-from-markdown": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", - "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "mdast-util-to-string": "^2.0.0", - "micromark": "~2.11.0", - "parse-entities": "^2.0.0", - "unist-util-stringify-position": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/mdast-util-to-hast": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-10.2.0.tgz", - "integrity": "sha512-JoPBfJ3gBnHZ18icCwHR50orC9kNH81tiR1gs01D8Q5YpV6adHNO9nKNuFBCJQ941/32PT1a63UF/DitmS3amQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^3.0.0", - "@types/unist": "^2.0.0", - "mdast-util-definitions": "^4.0.0", - "mdurl": "^1.0.0", - "unist-builder": "^2.0.0", - "unist-util-generated": "^1.0.0", - "unist-util-position": "^3.0.0", - "unist-util-visit": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/inline-style-parser": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", - "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", - "license": "MIT" - }, - "node_modules/@refinedev/antd/node_modules/bail": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", - "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "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", - "engines": { - "node": ">=4" - } - }, - "node_modules/@refinedev/antd/node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@refinedev/antd/node_modules/trough": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", - "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/@refinedev/antd/node_modules/unist-util-is": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", - "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/unist-util-visit-parents": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", - "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/unist-util-stringify-position": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", - "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/vfile-message": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", - "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/mdast-util-gfm-autolink-literal": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-0.1.3.tgz", - "integrity": "sha512-GjmLjWrXg1wqMIO9+ZsRik/s7PLwTaeCHVB7vRxUwLntZc8mzmTsLVr6HW1yLokcnhfURsn5zmSVdi3/xWWu1A==", - "license": "MIT", - "dependencies": { - "ccount": "^1.0.0", - "mdast-util-find-and-replace": "^1.1.0", - "micromark": "^2.11.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/mdast-util-gfm-strikethrough": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-0.2.3.tgz", - "integrity": "sha512-5OQLXpt6qdbttcDG/UxYY7Yjj3e8P7X16LzvpX8pIQPYJ/C2Z1qFGMmcw+1PZMUM3Z8wt8NRfYTvCni93mgsgA==", - "license": "MIT", - "dependencies": { - "mdast-util-to-markdown": "^0.6.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/mdast-util-gfm-table": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-0.1.6.tgz", - "integrity": "sha512-j4yDxQ66AJSBwGkbpFEp9uG/LS1tZV3P33fN1gkyRB2LoRL+RR3f76m0HPHaby6F4Z5xr9Fv1URmATlRRUIpRQ==", - "license": "MIT", - "dependencies": { - "markdown-table": "^2.0.0", - "mdast-util-to-markdown": "~0.6.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/mdast-util-gfm-task-list-item": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-0.1.6.tgz", - "integrity": "sha512-/d51FFIfPsSmCIRNp7E6pozM9z1GYPIkSy1urQ8s/o4TC22BZ7DqfHFWiqBD23bc7J3vV1Fc9O4QIHBlfuit8A==", - "license": "MIT", - "dependencies": { - "mdast-util-to-markdown": "~0.6.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/mdast-util-to-markdown": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz", - "integrity": "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "longest-streak": "^2.0.0", - "mdast-util-to-string": "^2.0.0", - "parse-entities": "^2.0.0", - "repeat-string": "^1.0.0", - "zwitch": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/micromark": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", - "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "debug": "^4.0.0", - "parse-entities": "^2.0.0" - } - }, - "node_modules/@refinedev/antd/node_modules/micromark-extension-gfm-autolink-literal": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-0.5.7.tgz", - "integrity": "sha512-ePiDGH0/lhcngCe8FtH4ARFoxKTUelMp4L7Gg2pujYD5CSMb9PbblnyL+AAMud/SNMyusbS2XDSiPIRcQoNFAw==", - "license": "MIT", - "dependencies": { - "micromark": "~2.11.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/micromark-extension-gfm-strikethrough": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-0.6.5.tgz", - "integrity": "sha512-PpOKlgokpQRwUesRwWEp+fHjGGkZEejj83k9gU5iXCbDG+XBA92BqnRKYJdfqfkrRcZRgGuPuXb7DaK/DmxOhw==", - "license": "MIT", - "dependencies": { - "micromark": "~2.11.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/micromark-extension-gfm-table": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-0.4.3.tgz", - "integrity": "sha512-hVGvESPq0fk6ALWtomcwmgLvH8ZSVpcPjzi0AjPclB9FsVRgMtGZkUcpE0zgjOCFAznKepF4z3hX8z6e3HODdA==", - "license": "MIT", - "dependencies": { - "micromark": "~2.11.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/micromark-extension-gfm-tagfilter": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-0.3.0.tgz", - "integrity": "sha512-9GU0xBatryXifL//FJH+tAZ6i240xQuFrSL7mYi8f4oZSbc+NvXjkrHemeYP0+L4ZUT+Ptz3b95zhUZnMtoi/Q==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/micromark-extension-gfm-task-list-item": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-0.3.3.tgz", - "integrity": "sha512-0zvM5iSLKrc/NQl84pZSjGo66aTGd57C1idmlWmE87lkMcXrTxg1uXa/nXomxJytoje9trP0NDLvw4bZ/Z/XCQ==", - "license": "MIT", - "dependencies": { - "micromark": "~2.11.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/stackframe": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", - "license": "MIT" - }, - "node_modules/string-convert": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", - "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", - "license": "MIT" - }, - "node_modules/@ant-design/pro-layout/node_modules/@ant-design/fast-color": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", - "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.24.7" - }, - "engines": { - "node": ">=8.x" - } - }, - "node_modules/@ant-design/pro-utils/node_modules/@ant-design/colors": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", - "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", - "license": "MIT", - "dependencies": { - "@ant-design/fast-color": "^2.0.6" - } - }, - "node_modules/@refinedev/antd/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/@refinedev/antd/node_modules/mdast-util-to-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", - "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/parse-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", - "license": "MIT", - "dependencies": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/mdast-util-definitions": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz", - "integrity": "sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==", - "license": "MIT", - "dependencies": { - "unist-util-visit": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", - "license": "MIT" - }, - "node_modules/unist-builder": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-2.0.3.tgz", - "integrity": "sha512-f98yt5pnlMWlzP539tPc4grGMsFaQQlP/vM396b00jngsiINumNmsY8rkXjfoi1c6QaM8nQ3vaGDuoKWbe/1Uw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-generated": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-1.1.6.tgz", - "integrity": "sha512-cln2Mm1/CZzN5ttGK7vkoGw+RZ8VcUH6BtGbq98DDtRGquAAOXig1mrBQYelOwMXYS8rK+vZDyyojSjp7JX+Lg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/unist-util-position": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-3.1.0.tgz", - "integrity": "sha512-w+PkwCbYSFw8vpgWD0v7zRCl1FpY3fjDSQ3/N/wNd9Ffa4gPi8+4keqt99N3XW6F99t/mUzp2xAhNmfKWp95QA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/ccount": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-1.1.0.tgz", - "integrity": "sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/@refinedev/antd/node_modules/mdast-util-find-and-replace": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-1.1.1.tgz", - "integrity": "sha512-9cKl33Y21lyckGzpSmEQnIDjEfeeWelN5s1kUW1LwdB0Fkuq2u+4GdqcGEygYxJE8GVqCl0741bYXHgamfWAZA==", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^4.0.0", - "unist-util-is": "^4.0.0", - "unist-util-visit-parents": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/markdown-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", - "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", - "license": "MIT", - "dependencies": { - "repeat-string": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/@refinedev/antd/node_modules/longest-streak": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", - "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/@refinedev/antd/node_modules/zwitch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", - "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/@ant-design/pro-utils/node_modules/@ant-design/fast-color": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", - "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.24.7" - }, - "engines": { - "node": ">=8.x" - } - }, - "node_modules/@refinedev/antd/node_modules/character-entities": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/@refinedev/antd/node_modules/character-entities-legacy": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/@refinedev/antd/node_modules/character-reference-invalid": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/@refinedev/antd/node_modules/is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/@refinedev/antd/node_modules/is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/@refinedev/antd/node_modules/is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/mdast-util-definitions/node_modules/unist-util-visit": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz", - "integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0", - "unist-util-visit-parents": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@refinedev/antd/node_modules/is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/mdast-util-definitions/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/mdast-util-definitions/node_modules/unist-util-is": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", - "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-definitions/node_modules/unist-util-visit-parents": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz", - "integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } } } } diff --git a/frontend/package.json b/frontend/package.json index 9f1c2ea1..3309278f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,17 +17,12 @@ "worker:rectification-v4": "tsx scripts/rectification-v4-worker.mts" }, "dependencies": { - "@ant-design/icons": "^6.3.2", "@base-ui/react": "^1.6.0", "@gsap/react": "^2.1.2", "@mastra/core": "^1.50.1", - "@refinedev/antd": "^6.0.3", - "@refinedev/core": "^5.0.12", - "@refinedev/nextjs-router": "^7.0.5", "@supabase/ssr": "^0.12.3", "@supabase/supabase-js": "^2.110.5", "@tailwindcss/postcss": "^4.3.2", - "antd": "^5.29.3", "better-auth": "1.6.23", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -42,7 +37,6 @@ "react-dom": "19.2.4", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", - "server-only": "^0.0.1", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.2", diff --git a/frontend/scripts/rectification-v4-worker.mts b/frontend/scripts/rectification-v4-worker.mts index 72cac9cd..93cf685a 100644 --- a/frontend/scripts/rectification-v4-worker.mts +++ b/frontend/scripts/rectification-v4-worker.mts @@ -1,6 +1,7 @@ import { setTimeout as sleep } from "node:timers/promises"; import { createRectificationV4CandidateEngine } from "../src/lib/rectification-v4/candidate-engine.ts"; import { createRectificationV4SupabaseStore } from "../src/lib/rectification-v4/supabase-store.ts"; +import { authorRectificationV4Question } from "../src/lib/rectification-v4/question-author.ts"; import { createRectificationV4Worker } from "../src/lib/rectification-v4/worker.ts"; import { createAdminSupabaseClient } from "../src/lib/supabase/admin-client-core.ts"; @@ -14,6 +15,7 @@ const worker = createRectificationV4Worker({ engine: createRectificationV4CandidateEngine({ apiBase: process.env.JYOTISH_API_BASE ?? "http://127.0.0.1:5200", }), + questionAuthor: authorRectificationV4Question, }); do { diff --git a/frontend/scripts/staging-image-manifest.mjs b/frontend/scripts/staging-image-manifest.mjs index 767c5644..bb27f13d 100644 --- a/frontend/scripts/staging-image-manifest.mjs +++ b/frontend/scripts/staging-image-manifest.mjs @@ -5,14 +5,8 @@ import { pathToFileURL } from "node:url"; const shaPattern = /^[0-9a-f]{40}$/; const digestPattern = /^sha256:[0-9a-f]{64}$/; const expectedKeys = ["git_sha", "api_digest", "web_digest"]; -const defaultRegistry = "ghcr.io/jesse-ux"; -const acrRepository = "crpi-d1feco6itet73spp.cn-hongkong.personal.cr.aliyuncs.com/copse/jyotisha"; -const registryPattern = /^(?:[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?)(?::[1-9][0-9]{0,4})?(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*$/; -export function parseStagingImageManifest(text, expectedSha, registry = defaultRegistry) { - if (!registryPattern.test(registry)) { - throw new Error("invalid staging image registry"); - } +export function parseStagingImageManifest(text, expectedSha) { if (!shaPattern.test(expectedSha)) { throw new Error("invalid expected staging revision"); } @@ -43,13 +37,12 @@ export function parseStagingImageManifest(text, expectedSha, registry = defaultR } } - const sharedRepository = registry === acrRepository; return { gitSha: expectedSha, apiDigest: values.get("api_digest"), webDigest: values.get("web_digest"), - apiImage: `${sharedRepository ? registry : `${registry}/jyotisha-api`}@${values.get("api_digest")}`, - webImage: `${sharedRepository ? registry : `${registry}/jyotisha-web`}@${values.get("web_digest")}`, + apiImage: `ghcr.io/jesse-ux/jyotisha-api@${values.get("api_digest")}`, + webImage: `ghcr.io/jesse-ux/jyotisha-web@${values.get("web_digest")}`, }; } @@ -59,14 +52,13 @@ const invokedPath = process.argv[1] if (invokedPath === import.meta.url) { try { - const [manifestPath, expectedSha, registry] = process.argv.slice(2); + const [manifestPath, expectedSha] = process.argv.slice(2); if (!manifestPath || !expectedSha) { throw new Error("manifest path and expected revision are required"); } const manifest = parseStagingImageManifest( await readFile(manifestPath, "utf8"), expectedSha, - registry, ); process.stdout.write( [ diff --git a/frontend/src/app/admin/codes/page.tsx b/frontend/src/app/admin/codes/page.tsx index d195b258..f4cec373 100644 --- a/frontend/src/app/admin/codes/page.tsx +++ b/frontend/src/app/admin/codes/page.tsx @@ -1,27 +1,200 @@ "use client"; -import { useSearchParams } from "next/navigation"; +import Link from "next/link"; +import { FormEvent, useEffect, useRef, useState } from "react"; -import AuditLogsResource from "@/components/admin/audit-logs-resource"; -import CodesResource from "@/components/admin/codes-resource"; -import ConsultationsResource from "@/components/admin/consultations-resource"; -import CreditTransactionsResource from "@/components/admin/credit-transactions-resource"; -import UsersResource from "@/components/admin/users-resource"; +type CodeRecord = { + id: string; + mask: string; + credits: number; + expiresAt: string | null; + redeemedBy: string | null; + redeemedEmail: string | null; + redeemedAt: string | null; + note: string | null; + createdAt: string; +}; +type GeneratedCode = { code: string; credits: number; expiresAt: string | null }; -const resourceComponents = { - codes: CodesResource, - users: UsersResource, - "credit-transactions": CreditTransactionsResource, - consultations: ConsultationsResource, - "audit-logs": AuditLogsResource, -} as const; +const previewCodes: CodeRecord[] = [ + { id: "preview-1", mask: "JYOT-••••-7Q9K", credits: 12, expiresAt: "2026-12-31T15:59:00.000Z", redeemedBy: null, redeemedEmail: null, redeemedAt: null, note: "秋季体验", createdAt: "2026-07-16T02:20:00.000Z" }, + { id: "preview-2", mask: "JYOT-••••-2M8A", credits: 6, expiresAt: null, redeemedBy: "preview-user", redeemedEmail: "linyao@example.com", redeemedAt: "2026-07-15T08:30:00.000Z", note: "访谈用户", createdAt: "2026-07-14T03:10:00.000Z" }, + { id: "preview-3", mask: "JYOT-••••-4D1R", credits: 20, expiresAt: "2026-07-01T15:59:00.000Z", redeemedBy: null, redeemedEmail: null, redeemedAt: null, note: null, createdAt: "2026-06-10T06:45:00.000Z" }, +]; -export default function AdminResourcesPage() { - const requested = useSearchParams().get("resource") ?? "codes"; - const Resource = resourceComponents[ - requested in resourceComponents - ? requested as keyof typeof resourceComponents - : "codes" - ]; - return ; +const dateFormatter = new Intl.DateTimeFormat("zh-CN", { + dateStyle: "medium", + timeStyle: "short", + timeZone: "Asia/Taipei", +}); + +function apiMessage(payload: unknown, fallback: string) { + if (!payload || typeof payload !== "object") return fallback; + const data = payload as Record; + return [data.message, data.error].find((value) => typeof value === "string") as string || fallback; +} + +function redirectForAuth(response: Response) { + if (response.status === 401) window.location.assign("/login"); + if (response.status === 403) window.location.assign("/"); +} + +function codeStatus(code: CodeRecord) { + if (code.redeemedAt) return "已兑换"; + if (code.expiresAt && new Date(code.expiresAt).getTime() <= Date.now()) return "已过期"; + return "可用"; +} + +function formatDate(value: string | null) { + return value ? dateFormatter.format(new Date(value)) : "—"; +} + +export default function AdminCodesPage() { + const [codes, setCodes] = useState([]); + const [generated, setGenerated] = useState([]); + const [credits, setCredits] = useState(10); + const [count, setCount] = useState(1); + const [expiresAt, setExpiresAt] = useState(""); + const [note, setNote] = useState(""); + const [loading, setLoading] = useState(true); + const [creating, setCreating] = useState(false); + const previewMode = useRef(false); + const [error, setError] = useState(""); + const [copyNotice, setCopyNotice] = useState(""); + + useEffect(() => { + if (process.env.NODE_ENV === "development" && new URLSearchParams(window.location.search).get("preview") === "admin") { + const previewFrame = window.requestAnimationFrame(() => { + previewMode.current = true; + setCodes(previewCodes); + setLoading(false); + }); + return () => window.cancelAnimationFrame(previewFrame); + } + + const controller = new AbortController(); + void fetch("/api/admin/codes", { signal: controller.signal, cache: "no-store" }) + .then(async (response) => { + redirectForAuth(response); + const payload = await response.json().catch(() => null); + if (!response.ok) throw new Error(apiMessage(payload, "暂时无法读取兑换码")); + setCodes((payload as { codes: CodeRecord[] }).codes); + }) + .catch((caught) => { + if ((caught as Error).name !== "AbortError") setError(caught instanceof Error ? caught.message : "暂时无法读取兑换码"); + }) + .finally(() => setLoading(false)); + return () => controller.abort(); + }, []); + + async function reloadCodes() { + const response = await fetch("/api/admin/codes", { cache: "no-store" }); + redirectForAuth(response); + const payload = await response.json().catch(() => null); + if (!response.ok) throw new Error(apiMessage(payload, "暂时无法刷新兑换码")); + setCodes((payload as { codes: CodeRecord[] }).codes); + } + + async function createCodes(event: FormEvent) { + event.preventDefault(); + if (creating) return; + setCreating(true); + setError(""); + setGenerated([]); + setCopyNotice(""); + if (process.env.NODE_ENV === "development" && previewMode.current) { + setGenerated(Array.from({ length: count }, (_, index) => ({ + code: `PREVIEW-${String(index + 1).padStart(2, "0")}-JYOTISH`, + credits, + expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null, + }))); + setCreating(false); + return; + } + try { + const response = await fetch("/api/admin/codes", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + credits, + count, + ...(expiresAt ? { expiresAt: new Date(expiresAt).toISOString() } : {}), + ...(note.trim() ? { note: note.trim() } : {}), + }), + }); + redirectForAuth(response); + const payload = await response.json().catch(() => null); + if (!response.ok) throw new Error(apiMessage(payload, "生成兑换码失败")); + setGenerated((payload as { codes: GeneratedCode[] }).codes); + await reloadCodes(); + } catch (caught) { + setError(caught instanceof Error ? caught.message : "生成兑换码失败"); + } finally { + setCreating(false); + } + } + + async function copy(text: string) { + try { + await navigator.clipboard.writeText(text); + setCopyNotice("已复制到剪贴板"); + } catch { + setCopyNotice("无法自动复制,请手动选择兑换码"); + } + } + + return ( +
+
+

兑换码管理

+ 返回对话 +
+ +
+
+

生成兑换码

完整兑换码只在本次生成结果中显示,请立即复制保存。

+
+ + + + + +
+ {error &&

{error}

} +
+ + {generated.length > 0 && ( +
+
+

本次生成的完整码

离开或刷新页面后将不再显示。

+ +
+
+ {generated.map((item) => ( +
{item.code}{item.credits} 点
+ ))} +
+ {copyNotice &&

{copyNotice}

} +
+ )} + +
+

兑换码状态

{loading ? "正在读取…" : `${codes.length} 条记录`}

+
+ + + + {codes.map((code) => ( + + + + ))} + {!loading && codes.length === 0 && } + +
兑换码点数状态有效期兑换账户兑换时间备注创建时间
{code.mask}{code.credits}{codeStatus(code)}{formatDate(code.expiresAt)}{code.redeemedEmail || code.redeemedBy || "—"}{formatDate(code.redeemedAt)}{code.note || "—"}{formatDate(code.createdAt)}
尚未生成兑换码
+
+
+
+
+ ); } diff --git a/frontend/src/app/admin/layout.tsx b/frontend/src/app/admin/layout.tsx index c7e13d4b..a5e09cd3 100644 --- a/frontend/src/app/admin/layout.tsx +++ b/frontend/src/app/admin/layout.tsx @@ -1,21 +1,18 @@ -import "@refinedev/antd/dist/reset.css"; -import "antd/dist/reset.css"; -import type { ReactNode } from "react"; +import { ReactNode } from "react"; import { redirect } from "next/navigation"; - -import { AdminApp } from "@/components/admin/admin-app"; -import { AdminAuthorizationError, requireAdminSession } from "@/lib/admin/auth"; +import { isAdminEmail } from "@/lib/supabase/admin"; +import { createServerSupabaseClient } from "@/lib/supabase/server"; export const dynamic = "force-dynamic"; export default async function AdminLayout({ children }: { children: ReactNode }) { - try { - await requireAdminSession("read"); - } catch (error) { - if (error instanceof AdminAuthorizationError) { - redirect(error.status === 401 ? "/login" : "/"); - } - throw error; - } - return {children}; + if (process.env.NODE_ENV === "development" && process.env.ENABLE_ADMIN_PREVIEW === "1") return children; + + const supabase = await createServerSupabaseClient(); + const { data: { user } } = await supabase.auth.getUser(); + + if (!user) redirect("/login"); + if (!isAdminEmail(user.email)) redirect("/"); + + return children; } diff --git a/frontend/src/app/admin/packages/page.tsx b/frontend/src/app/admin/packages/page.tsx deleted file mode 100644 index 2ab9a048..00000000 --- a/frontend/src/app/admin/packages/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { PackageManagement } from "@/components/admin/package-management"; - -export default function AdminPackagesPage() { - return ; -} diff --git a/frontend/src/app/admin/payments/page.tsx b/frontend/src/app/admin/payments/page.tsx deleted file mode 100644 index bf3b1f10..00000000 --- a/frontend/src/app/admin/payments/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import PaymentManagement from "@/components/admin/payment-management"; - -export default function AdminPaymentsPage() { - return ; -} diff --git a/frontend/src/app/admin/route.ts b/frontend/src/app/admin/route.ts deleted file mode 100644 index cc04f81d..00000000 --- a/frontend/src/app/admin/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { AdminAuthorizationError, requireAdminSession } from "@/lib/admin/auth"; - -export async function GET() { - try { - await requireAdminSession("read"); - return new Response(null, { - status: 307, - headers: { location: "/admin/codes" }, - }); - } catch (error) { - if (error instanceof AdminAuthorizationError) { - return new Response(null, { - status: 307, - headers: { location: error.status === 401 ? "/login" : "/" }, - }); - } - throw error; - } -} diff --git a/frontend/src/app/admin/users/page.tsx b/frontend/src/app/admin/users/page.tsx deleted file mode 100644 index 77536ffc..00000000 --- a/frontend/src/app/admin/users/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { FormEvent, useEffect, useState } from "react"; - -type AdminUser = { userId: string | null; email: string | null; createdAt: string | null; source: "env" | "database" }; - -export default function AdminUsersPage() { - const [users, setUsers] = useState([]); - const [email, setEmail] = useState(""); - const [error, setError] = useState(""); - async function load() { - const response = await fetch("/api/admin/users", { cache: "no-store" }); - const payload = await response.json().catch(() => null); - if (!response.ok) throw new Error(payload?.error || "暂时无法读取管理员列表"); - setUsers(payload.users); - } - useEffect(() => { void load().catch((caught) => setError(caught.message)); }, []); - async function add(event: FormEvent) { - event.preventDefault(); setError(""); - const response = await fetch("/api/admin/users", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email }) }); - const payload = await response.json().catch(() => null); - if (!response.ok) { setError(payload?.error || "添加失败"); return; } - setEmail(""); await load(); - } - async function revoke(userId: string) { - const response = await fetch("/api/admin/users", { method: "DELETE", headers: { "content-type": "application/json" }, body: JSON.stringify({ userId }) }); - if (!response.ok) { const payload = await response.json().catch(() => null); setError(payload?.error || "撤销失败"); return; } - await load(); - } - return

管理员管理

兑换码管理

添加管理员

仅能添加已经注册的 Supabase 用户。

{error &&

{error}

}

当前管理员

{users.length} 位

{users.map((user) => )}
邮箱来源添加时间操作
{user.email || "—"}{user.source === "env" ? "环境配置" : "后台配置"}{user.createdAt ? new Date(user.createdAt).toLocaleString("zh-CN") : "—"}{user.userId && }
; -} diff --git a/frontend/src/app/api/account/route.ts b/frontend/src/app/api/account/route.ts index d40a93e1..552f3c49 100644 --- a/frontend/src/app/api/account/route.ts +++ b/frontend/src/app/api/account/route.ts @@ -8,7 +8,7 @@ import { applyAccountProfileConcurrencyGuards, resolveAccountBirthTimeApplicationPatch, } from "@/lib/account-profile-patch"; -import { createAdminSupabaseClient, isAdminUser } from "@/lib/supabase/admin"; +import { createAdminSupabaseClient, isAdminEmail } from "@/lib/supabase/admin"; import { isSupabaseConfigurationError, } from "@/lib/supabase/config"; @@ -109,14 +109,11 @@ export async function GET() { profile, Array.isArray(rectificationCaseRows) ? rectificationCaseRows : [], ); - const isAdmin = await isAdminUser(user); - const adminUrl = isAdmin ? "/admin/codes" : null; return NextResponse.json({ user: { id: user.id, email: user.email ?? null }, credits: profile.credits, - isAdmin, - adminUrl, + isAdmin: isAdminEmail(user.email), rectificationPriceCredits, hasConfirmedBirthTime: profile.birth_time_status === "confirmed" && typeof profile.active_birth_time === "string", diff --git a/frontend/src/app/api/admin/audit-logs/route.ts b/frontend/src/app/api/admin/audit-logs/route.ts deleted file mode 100644 index ffdf00b1..00000000 --- a/frontend/src/app/api/admin/audit-logs/route.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { NextResponse } from "next/server"; - -import { requireAdminSession } from "@/lib/admin/auth"; -import { pageOffset, queryAdminRows } from "@/lib/admin/database"; -import { - adminErrorResponse, - invalidQueryResponse, - parseListQuery, - readonlyAdminMutation, -} from "@/lib/admin/http"; - -export const runtime = "nodejs"; - -type AuditRow = { - id: string; - actor_user_id: string; - actor_email: string; - actor_role: string; - action: string; - target_type: string; - target_id: string; - before_value: Record | null; - after_value: Record | null; - request_id: string; - created_at: Date; - total_count: string; -}; - -const sortColumns = new Map([ - ["createdAt", "a.created_at"], - ["action", "a.action"], - ["actorEmail", "a.actor_email"], -]); - -export const POST = readonlyAdminMutation; -export const PUT = readonlyAdminMutation; -export const PATCH = readonlyAdminMutation; -export const DELETE = readonlyAdminMutation; - -export async function GET(request: Request) { - try { - await requireAdminSession(); - const parsed = parseListQuery(request); - if (!parsed.success) return invalidQueryResponse(parsed.error.flatten()); - const { page, pageSize, sort, order, q, status } = parsed.data; - const values: unknown[] = []; - const conditions: string[] = []; - if (q) { - values.push(`%${q}%`); - conditions.push(`(a.actor_email ilike $${values.length} or a.request_id ilike $${values.length})`); - } - if (status) { - values.push(status); - conditions.push(`a.action = $${values.length}`); - } - values.push(pageSize, pageOffset(page, pageSize)); - const sortColumn = sortColumns.get(sort ?? "createdAt") ?? "a.created_at"; - const rows = await queryAdminRows(` - select a.*, count(*) over()::text as total_count - from audit.admin_audit_logs a - ${conditions.length ? `where ${conditions.join(" and ")}` : ""} - order by ${sortColumn} ${order === "asc" ? "asc" : "desc"}, a.id asc - limit $${values.length - 1} offset $${values.length} - `, values); - return NextResponse.json({ - data: rows.map((row) => ({ - id: row.id, - actorUserId: row.actor_user_id, - actorEmail: row.actor_email, - actorRole: row.actor_role, - action: row.action, - targetType: row.target_type, - targetId: row.target_id, - before: row.before_value, - after: row.after_value, - requestId: row.request_id, - createdAt: row.created_at.toISOString(), - })), - total: Number(rows[0]?.total_count ?? 0), - }); - } catch (error) { - return adminErrorResponse(error); - } -} diff --git a/frontend/src/app/api/admin/codes/[id]/route.ts b/frontend/src/app/api/admin/codes/[id]/route.ts deleted file mode 100644 index c53af63d..00000000 --- a/frontend/src/app/api/admin/codes/[id]/route.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { NextResponse } from "next/server"; -import { z } from "zod"; - -import { requireAdminSession } from "@/lib/admin/auth"; -import { runCodeRpc } from "@/lib/admin/codes"; -import { - adminErrorResponse, - invalidQueryResponse, - requestId, -} from "@/lib/admin/http"; - -export const runtime = "nodejs"; - -const paramsSchema = z.object({ id: z.string().uuid() }); -const updateCodeSchema = z.object({ - note: z.string().trim().max(500).nullable().optional(), - expiresAt: z.string().datetime({ offset: true }).nullable().optional(), -}).refine((value) => "note" in value || "expiresAt" in value, { - message: "至少提供一个可修改字段", -}); - -export async function PATCH( - request: Request, - context: { params: Promise<{ id: string }> }, -) { - try { - const session = await requireAdminSession("write"); - const parsedParams = paramsSchema.safeParse(await context.params); - const parsedBody = updateCodeSchema.safeParse(await request.json().catch(() => null)); - if (!parsedParams.success || !parsedBody.success) { - return invalidQueryResponse(); - } - const body = parsedBody.data; - const rows = await runCodeRpc( - "admin_update_redemption_code", - session, - requestId(request), - { - p_code_id: parsedParams.data.id, - p_set_note: "note" in body, - p_note: body.note ?? null, - p_set_expires_at: "expiresAt" in body, - p_expires_at: body.expiresAt ?? null, - }, - ); - return NextResponse.json({ data: rows[0] }); - } catch (error) { - return adminErrorResponse(error); - } -} - -export async function DELETE( - request: Request, - context: { params: Promise<{ id: string }> }, -) { - try { - const session = await requireAdminSession("write"); - const parsed = paramsSchema.safeParse(await context.params); - if (!parsed.success) return invalidQueryResponse(); - const rows = await runCodeRpc( - "admin_revoke_redemption_code", - session, - requestId(request), - { p_code_id: parsed.data.id }, - ); - return NextResponse.json({ data: rows[0] }); - } catch (error) { - return adminErrorResponse(error); - } -} diff --git a/frontend/src/app/api/admin/codes/route.ts b/frontend/src/app/api/admin/codes/route.ts index bab2b438..e4f22605 100644 --- a/frontend/src/app/api/admin/codes/route.ts +++ b/frontend/src/app/api/admin/codes/route.ts @@ -1,143 +1,113 @@ import { NextResponse } from "next/server"; import { z } from "zod"; - -import { requireAdminSession } from "@/lib/admin/auth"; -import { mapCode, runCodeRpc, type RedemptionCodeRecord } from "@/lib/admin/codes"; -import { pageOffset, queryAdminRows } from "@/lib/admin/database"; import { - adminErrorResponse, - invalidQueryResponse, - parseListQuery, - requestId, -} from "@/lib/admin/http"; + createAdminSupabaseClient, + isAdminEmail, +} from "@/lib/supabase/admin"; import { generateRedeemCode, hashRedeemCode, maskRedeemCode, } from "@/lib/supabase/codes"; +import { + isSupabaseConfigurationError, + SupabaseConfigurationError, +} from "@/lib/supabase/config"; +import { createServerSupabaseClient } from "@/lib/supabase/server"; export const runtime = "nodejs"; const createCodesSchema = z.object({ credits: z.number().int().positive().max(1_000_000), count: z.number().int().min(1).max(100), - expiresAt: z.string().datetime({ offset: true }).nullable().optional(), - note: z.string().trim().max(500).nullable().optional(), + expiresAt: z.string().datetime({ offset: true }).optional(), + note: z.string().trim().max(500).optional(), }); -type CodeRow = { - id: string; - code_mask: string; - credits: number; - expires_at: Date | null; - note: string | null; - created_at: Date; - redeemed_by: string | null; - redeemed_email: string | null; - redeemed_at: Date | null; - revoked_by: string | null; - revoked_at: Date | null; - total_count: string; -}; +async function requireAdmin() { + if (!process.env.ADMIN_EMAILS?.trim()) { + throw new SupabaseConfigurationError(["ADMIN_EMAILS"]); + } -const sortColumns = new Map([ - ["createdAt", "c.created_at"], - ["expiresAt", "c.expires_at"], - ["credits", "c.credits"], - ["status", "status"], -]); - -function serializedCodeRow(row: CodeRow) { - return mapCode({ - ...row, - expires_at: row.expires_at?.toISOString() ?? null, - created_at: row.created_at.toISOString(), - redeemed_at: row.redeemed_at?.toISOString() ?? null, - revoked_at: row.revoked_at?.toISOString() ?? null, - }); + const supabase = await createServerSupabaseClient(); + const { data: { user }, error } = await supabase.auth.getUser(); + if (error || !user) return { response: NextResponse.json({ error: "请先登录" }, { status: 401 }) }; + if (!isAdminEmail(user.email)) { + return { response: NextResponse.json({ error: "无管理员权限" }, { status: 403 }) }; + } + return { user }; } -export async function GET(request: Request) { +export async function GET() { try { - await requireAdminSession(); - const parsed = parseListQuery(request); - if (!parsed.success) return invalidQueryResponse(parsed.error.flatten()); - const { page, pageSize, sort, order, q, status } = parsed.data; - const values: unknown[] = []; - const conditions: string[] = []; - if (q) { - values.push(`%${q}%`); - conditions.push(`(c.code_mask ilike $${values.length} or c.note ilike $${values.length})`); + const auth = await requireAdmin(); + if ("response" in auth) return auth.response; + + const admin = createAdminSupabaseClient(); + const { data, error } = await admin + .from("redemption_codes") + .select("id,code_mask,credits,expires_at,note,created_at,redeemed_by,redeemed_email,redeemed_at") + .order("created_at", { ascending: false }) + .limit(100); + + if (error) { + return NextResponse.json({ error: "暂时无法读取兑换码列表" }, { status: 500 }); } - if (status && ["available", "expired", "redeemed", "revoked"].includes(status)) { - const clauses = { - available: "c.redeemed_at is null and c.revoked_at is null and (c.expires_at is null or c.expires_at > now())", - expired: "c.redeemed_at is null and c.revoked_at is null and c.expires_at <= now()", - redeemed: "c.redeemed_at is not null", - revoked: "c.revoked_at is not null", - }; - conditions.push(clauses[status as keyof typeof clauses]); - } - values.push(pageSize, pageOffset(page, pageSize)); - const sortColumn = sortColumns.get(sort ?? "createdAt") ?? "c.created_at"; - const rows = await queryAdminRows(` - select c.id, c.code_mask, c.credits, c.expires_at, c.note, - c.created_at, c.redeemed_by, c.redeemed_email, c.redeemed_at, - c.revoked_by, c.revoked_at, - case - when c.redeemed_at is not null then 'redeemed' - when c.revoked_at is not null then 'revoked' - when c.expires_at is not null and c.expires_at <= now() then 'expired' - else 'available' - end as status, - count(*) over()::text as total_count - from public.redemption_codes c - ${conditions.length ? `where ${conditions.join(" and ")}` : ""} - order by ${sortColumn} ${order === "asc" ? "asc" : "desc"}, c.id asc - limit $${values.length - 1} offset $${values.length} - `, values); + return NextResponse.json({ - data: rows.map(serializedCodeRow), - total: Number(rows[0]?.total_count ?? 0), + codes: data.map((code) => ({ + id: code.id, + mask: code.code_mask, + credits: code.credits, + expiresAt: code.expires_at, + note: code.note, + createdAt: code.created_at, + redeemedBy: code.redeemed_by, + redeemedEmail: code.redeemed_email, + redeemedAt: code.redeemed_at, + })), }); } catch (error) { - return adminErrorResponse(error); + if (isSupabaseConfigurationError(error)) { + return NextResponse.json({ error: "Supabase 或管理员白名单尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 }); + } + return NextResponse.json({ error: "兑换码管理服务暂时不可用" }, { status: 500 }); } } export async function POST(request: Request) { try { - const session = await requireAdminSession("write"); + const auth = await requireAdmin(); + if ("response" in auth) return auth.response; + const parsed = createCodesSchema.safeParse(await request.json().catch(() => null)); - if (!parsed.success) return invalidQueryResponse(parsed.error.flatten()); - const plainCodes = Array.from({ length: parsed.data.count }, generateRedeemCode); - const records = plainCodes.map((code) => ({ - codeHash: hashRedeemCode(code), - codeMask: maskRedeemCode(code), - credits: parsed.data.credits, - expiresAt: parsed.data.expiresAt ?? null, - note: parsed.data.note || null, - })); - const operationRequestId = requestId(request); - const stored = await runCodeRpc( - "admin_create_redemption_codes", - session, - operationRequestId, - { p_codes: records }, - ); - const byMask = new Map( - stored.map((record) => [record.mask, record]), - ); + if (!parsed.success) { + return NextResponse.json({ error: "兑换码参数不正确" }, { status: 400 }); + } + + const { credits, count, expiresAt, note } = parsed.data; + const codes = Array.from({ length: count }, generateRedeemCode); + const admin = createAdminSupabaseClient(); + const { error } = await admin.from("redemption_codes").insert(codes.map((code) => ({ + code_hash: hashRedeemCode(code), + code_mask: maskRedeemCode(code), + credits, + expires_at: expiresAt ?? null, + note: note || null, + created_by: auth.user.id, + }))); + + if (error) { + return NextResponse.json({ error: "生成兑换码失败,请重试" }, { status: 500 }); + } + return NextResponse.json({ - data: { - id: operationRequestId, - generated: plainCodes.map((code) => ({ - ...(byMask.get(maskRedeemCode(code)) ?? {}), - code, - })), - }, + codes: codes.map((code) => ({ code, credits, expiresAt: expiresAt ?? null, note: note || null })), }, { status: 201 }); } catch (error) { - return adminErrorResponse(error); + if (isSupabaseConfigurationError(error)) { + return NextResponse.json({ error: "Supabase 或管理员白名单尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 }); + } + return NextResponse.json({ error: "兑换码管理服务暂时不可用" }, { status: 500 }); } } diff --git a/frontend/src/app/api/admin/consultations/route.ts b/frontend/src/app/api/admin/consultations/route.ts deleted file mode 100644 index 51ce4c53..00000000 --- a/frontend/src/app/api/admin/consultations/route.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { NextResponse } from "next/server"; - -import { requireAdminSession } from "@/lib/admin/auth"; -import { pageOffset, queryAdminRows } from "@/lib/admin/database"; -import { - adminErrorResponse, - invalidQueryResponse, - parseListQuery, - readonlyAdminMutation, -} from "@/lib/admin/http"; - -export const runtime = "nodejs"; - -type ConsultationRow = { - id: string; - user_id: string; - email: string | null; - request_id: string; - status: string; - created_at: Date; - updated_at: Date; - total_count: string; -}; - -const sortColumns = new Map([ - ["createdAt", "c.created_at"], - ["updatedAt", "c.updated_at"], - ["status", "c.status"], -]); - -export const POST = readonlyAdminMutation; -export const PUT = readonlyAdminMutation; -export const PATCH = readonlyAdminMutation; -export const DELETE = readonlyAdminMutation; - -export async function GET(request: Request) { - try { - await requireAdminSession(); - const parsed = parseListQuery(request); - if (!parsed.success) return invalidQueryResponse(parsed.error.flatten()); - const { page, pageSize, sort, order, q, status } = parsed.data; - const values: unknown[] = []; - const conditions: string[] = []; - if (q) { - values.push(`%${q}%`); - conditions.push(`(p.email ilike $${values.length} or c.request_id ilike $${values.length})`); - } - if (status && ["reserved", "completed", "cancelled"].includes(status)) { - values.push(status); - conditions.push(`c.status = $${values.length}`); - } - values.push(pageSize, pageOffset(page, pageSize)); - const sortColumn = sortColumns.get(sort ?? "createdAt") ?? "c.created_at"; - const rows = await queryAdminRows(` - select c.user_id || ':' || c.request_id as id, c.user_id, p.email, - c.request_id, c.status, c.created_at, c.updated_at, - count(*) over()::text as total_count - from public.consultation_requests c - left join public.profiles p on p.id = c.user_id - ${conditions.length ? `where ${conditions.join(" and ")}` : ""} - order by ${sortColumn} ${order === "asc" ? "asc" : "desc"}, c.request_id asc - limit $${values.length - 1} offset $${values.length} - `, values); - return NextResponse.json({ - data: rows.map((row) => ({ - id: row.id, - userId: row.user_id, - email: row.email, - requestId: row.request_id, - status: row.status, - createdAt: row.created_at.toISOString(), - updatedAt: row.updated_at.toISOString(), - })), - total: Number(rows[0]?.total_count ?? 0), - }); - } catch (error) { - return adminErrorResponse(error); - } -} diff --git a/frontend/src/app/api/admin/credit-transactions/route.ts b/frontend/src/app/api/admin/credit-transactions/route.ts deleted file mode 100644 index adf86190..00000000 --- a/frontend/src/app/api/admin/credit-transactions/route.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { NextResponse } from "next/server"; - -import { requireAdminSession } from "@/lib/admin/auth"; -import { pageOffset, queryAdminRows } from "@/lib/admin/database"; -import { - adminErrorResponse, - invalidQueryResponse, - parseListQuery, - readonlyAdminMutation, -} from "@/lib/admin/http"; - -export const runtime = "nodejs"; - -type TransactionRow = { - id: string; - user_id: string; - email: string | null; - transaction_type: string; - amount: number; - balance_after: number; - request_id: string; - model: string | null; - input_tokens: number | null; - output_tokens: number | null; - created_at: Date; - total_count: string; -}; - -const sortColumns = new Map([ - ["createdAt", "t.created_at"], - ["amount", "t.amount"], - ["balanceAfter", "t.balance_after"], - ["type", "t.transaction_type"], -]); - -export const POST = readonlyAdminMutation; -export const PUT = readonlyAdminMutation; -export const PATCH = readonlyAdminMutation; -export const DELETE = readonlyAdminMutation; - -export async function GET(request: Request) { - try { - await requireAdminSession(); - const parsed = parseListQuery(request); - if (!parsed.success) return invalidQueryResponse(parsed.error.flatten()); - const { page, pageSize, sort, order, q, status } = parsed.data; - const values: unknown[] = []; - const conditions: string[] = []; - if (q) { - values.push(`%${q}%`); - conditions.push(`(p.email ilike $${values.length} or t.request_id ilike $${values.length})`); - } - if (status && ["redeem", "reserve", "refund"].includes(status)) { - values.push(status); - conditions.push(`t.transaction_type = $${values.length}`); - } - values.push(pageSize, pageOffset(page, pageSize)); - const sortColumn = sortColumns.get(sort ?? "createdAt") ?? "t.created_at"; - const rows = await queryAdminRows(` - select t.id, t.user_id, p.email, t.transaction_type, t.amount, - t.balance_after, t.request_id, t.model, t.input_tokens, - t.output_tokens, t.created_at, count(*) over()::text as total_count - from public.credit_transactions t - left join public.profiles p on p.id = t.user_id - ${conditions.length ? `where ${conditions.join(" and ")}` : ""} - order by ${sortColumn} ${order === "asc" ? "asc" : "desc"}, t.id asc - limit $${values.length - 1} offset $${values.length} - `, values); - return NextResponse.json({ - data: rows.map((row) => ({ - id: row.id, - userId: row.user_id, - email: row.email, - type: row.transaction_type, - amount: row.amount, - balanceAfter: row.balance_after, - requestId: row.request_id, - model: row.model, - inputTokens: row.input_tokens, - outputTokens: row.output_tokens, - createdAt: row.created_at.toISOString(), - })), - total: Number(rows[0]?.total_count ?? 0), - }); - } catch (error) { - return adminErrorResponse(error); - } -} diff --git a/frontend/src/app/api/admin/epay-settings/route.ts b/frontend/src/app/api/admin/epay-settings/route.ts deleted file mode 100644 index 50cdd7b5..00000000 --- a/frontend/src/app/api/admin/epay-settings/route.ts +++ /dev/null @@ -1,132 +0,0 @@ -import crypto from "node:crypto"; -import { NextResponse } from "next/server"; -import { z } from "zod"; -import { requireAdminSession } from "@/lib/admin/auth"; -import { isPostgresError, queryAdminRows } from "@/lib/admin/database"; -import { adminErrorResponse } from "@/lib/admin/http"; -import { suggestedEpayUrls } from "@/lib/epay/config"; -import { encryptEpayKey } from "@/lib/epay/encryption"; - -export const runtime = "nodejs"; - -const httpUrl = z.string().trim().min(1).max(2048).url().refine((value) => /^https?:\/\//i.test(value), "必须使用 HTTP(S)"); -const settingsSchema = z.object({ - gatewayUrl: httpUrl, - pid: z.string().trim().min(1).max(200), - notifyUrl: httpUrl, - returnUrl: httpUrl, - siteName: z.string().trim().min(1).max(100), - chatEnabled: z.boolean(), - newKey: z.string().min(1).max(1000).optional(), -}).strict(); - -type SettingsRow = { - gateway_url: string; - pid: string; - encrypted_key: string; - notify_url: string; - return_url: string; - site_name: string; - chat_enabled: boolean; - updated_at?: Date; -}; - -function publicSettings(row: SettingsRow, source: "database" | "environment") { - return { - gatewayUrl: row.gateway_url, - pid: row.pid, - notifyUrl: row.notify_url, - returnUrl: row.return_url, - siteName: row.site_name, - chatEnabled: row.chat_enabled, - keyConfigured: Boolean(row.encrypted_key), - complete: Boolean(row.gateway_url && row.pid && row.encrypted_key && row.notify_url && row.return_url && row.site_name), - source, - updatedAt: source === "database" ? row.updated_at?.toISOString() ?? null : null, - }; -} - -function environmentSettings() { - const defaults = suggestedEpayUrls(); - const row: SettingsRow = { - gateway_url: process.env.EPAY_GATEWAY_URL?.trim() || "", - pid: process.env.EPAY_PID?.trim() || "", - encrypted_key: process.env.EPAY_KEY?.trim() ? "configured" : "", - notify_url: process.env.EPAY_NOTIFY_URL?.trim() || defaults.notifyUrl, - return_url: process.env.EPAY_RETURN_URL?.trim() || defaults.returnUrl, - site_name: process.env.EPAY_SITE_NAME?.trim() || "Jyotisha", - chat_enabled: ["true", "1"].includes(process.env.EPAY_CHAT_ENABLED?.trim().toLowerCase() || ""), - }; - return publicSettings(row, "environment"); -} - -async function databaseRow() { - try { - const rows = await queryAdminRows(` - select gateway_url, pid, encrypted_key, notify_url, return_url, site_name, chat_enabled, updated_at - from public.epay_settings - where id = true - limit 1 - `); - return rows[0] ?? null; - } catch (error) { - if (isPostgresError(error) && error.code === "42P01") return null; - throw error; - } -} - -export async function GET() { - try { - await requireAdminSession("read"); - const row = await databaseRow(); - if (row) return NextResponse.json(publicSettings(row, "database")); - const settings = environmentSettings(); - return NextResponse.json(settings.complete || settings.keyConfigured - ? settings - : { ...settings, source: "unconfigured" }); - } catch (error) { - return adminErrorResponse(error); - } -} - -export async function PUT(request: Request) { - try { - const session = await requireAdminSession("write"); - const parsed = settingsSchema.safeParse(await request.json().catch(() => null)); - if (!parsed.success) return NextResponse.json({ error: "易支付配置参数不正确" }, { status: 400 }); - - const existing = await databaseRow(); - if (!existing && !parsed.data.newKey) { - return NextResponse.json({ error: "首次保存数据库配置时必须输入新的商户密钥" }, { status: 400 }); - } - const encryptedKey = parsed.data.newKey - ? encryptEpayKey(parsed.data.newKey) - : existing!.encrypted_key; - try { - const rows = await queryAdminRows(` - select * from public.admin_save_epay_settings( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 - ) - `, [ - session.user.id, - session.user.email, - session.role, - crypto.randomUUID(), - parsed.data.gatewayUrl.replace(/\/+$/, ""), - parsed.data.pid, - encryptedKey, - parsed.data.notifyUrl, - parsed.data.returnUrl, - parsed.data.siteName, - parsed.data.chatEnabled, - Boolean(parsed.data.newKey), - ]); - if (!rows[0]) return NextResponse.json({ error: "保存易支付配置失败" }, { status: 500 }); - return NextResponse.json(publicSettings(rows[0], "database")); - } catch { - return NextResponse.json({ error: "保存易支付配置失败" }, { status: 500 }); - } - } catch (error) { - return adminErrorResponse(error); - } -} diff --git a/frontend/src/app/api/admin/epay-settings/test/route.ts b/frontend/src/app/api/admin/epay-settings/test/route.ts deleted file mode 100644 index de157d34..00000000 --- a/frontend/src/app/api/admin/epay-settings/test/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NextResponse } from "next/server"; -import { AdminAuthorizationError, requireAdminSession } from "@/lib/admin/auth"; -import { adminErrorResponse } from "@/lib/admin/http"; -import { epaySubmitUrl, readEpayConfig } from "@/lib/epay/config"; -import { assertPublicGatewayUrl } from "@/lib/epay/gateway-policy"; - -export const runtime = "nodejs"; - -function reachableStatus(status: number) { - return status >= 200 && status < 500; -} - -export async function POST() { - try { - await requireAdminSession("write"); - const config = await readEpayConfig(); - const submitUrl = epaySubmitUrl(config.gatewayUrl); - await assertPublicGatewayUrl(submitUrl); - const startedAt = performance.now(); - let response = await fetch(submitUrl, { - method: "HEAD", - redirect: "manual", - signal: AbortSignal.timeout(8_000), - }); - if (response.status === 405 || response.status === 501) { - response = await fetch(submitUrl, { - method: "GET", - redirect: "manual", - signal: AbortSignal.timeout(8_000), - }); - } - const available = reachableStatus(response.status); - return NextResponse.json({ - available, - message: available ? "当前已保存的易支付配置可访问" : "当前已保存的易支付配置暂不可用", - latencyMs: Math.round(performance.now() - startedAt), - status: response.status, - }); - } catch (error) { - if (error instanceof AdminAuthorizationError) return adminErrorResponse(error); - return NextResponse.json({ - available: false, - message: "当前已保存的易支付配置暂不可用", - }); - } -} diff --git a/frontend/src/app/api/admin/packages/route.ts b/frontend/src/app/api/admin/packages/route.ts deleted file mode 100644 index 6601686d..00000000 --- a/frontend/src/app/api/admin/packages/route.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { NextResponse } from "next/server"; -import { z } from "zod"; -import { requireAdminSession } from "@/lib/admin/auth"; -import { queryAdminRows } from "@/lib/admin/database"; -import { adminErrorResponse } from "@/lib/admin/http"; - -export const runtime = "nodejs"; - -const schema = z.object({ - name: z.string().trim().min(1).max(80), - description: z.string().trim().max(500), - priceCents: z.number().int().positive().max(100_000_000), - credits: z.number().int().positive().max(10_000_000), - sortOrder: z.number().int().min(-100_000).max(100_000), - enabled: z.boolean(), -}).strict(); -const updateSchema = schema.extend({ id: z.string().uuid() }); -const idSchema = z.object({ id: z.string().uuid() }).strict(); - -type PackageRow = { - id: string; - name: string; - description: string; - price_cents: number; - credits: number; - sort_order: number; - enabled: boolean; - created_at: Date; - updated_at: Date; -}; - -function output(row: PackageRow) { - return { - id: row.id, - name: row.name, - description: row.description, - priceCents: row.price_cents, - credits: row.credits, - sortOrder: row.sort_order, - enabled: row.enabled, - createdAt: row.created_at.toISOString(), - updatedAt: row.updated_at.toISOString(), - }; -} - -export async function GET() { - try { - await requireAdminSession("read"); - const rows = await queryAdminRows(` - select id, name, description, price_cents, credits, sort_order, enabled, created_at, updated_at - from public.payment_packages - order by sort_order, created_at - `); - return NextResponse.json({ packages: rows.map(output) }); - } catch (error) { - return adminErrorResponse(error); - } -} - -export async function POST(request: Request) { - try { - const auth = await requireAdminSession("write"); - const parsed = schema.safeParse(await request.json().catch(() => null)); - if (!parsed.success) return NextResponse.json({ error: "套餐参数不正确" }, { status: 400 }); - const p = parsed.data; - const rows = await queryAdminRows(` - insert into public.payment_packages - (name, description, price_cents, credits, sort_order, enabled, created_by) - values ($1, $2, $3, $4, $5, $6, $7) - returning id, name, description, price_cents, credits, sort_order, enabled, created_at, updated_at - `, [p.name, p.description, p.priceCents, p.credits, p.sortOrder, p.enabled, auth.user.id]); - return NextResponse.json({ package: output(rows[0]) }, { status: 201 }); - } catch (error) { - return adminErrorResponse(error); - } -} - -export async function PATCH(request: Request) { - try { - await requireAdminSession("write"); - const parsed = updateSchema.safeParse(await request.json().catch(() => null)); - if (!parsed.success) return NextResponse.json({ error: "套餐参数不正确" }, { status: 400 }); - const p = parsed.data; - const rows = await queryAdminRows(` - update public.payment_packages - set name = $2, description = $3, price_cents = $4, credits = $5, - sort_order = $6, enabled = $7, updated_at = clock_timestamp() - where id = $1 - returning id, name, description, price_cents, credits, sort_order, enabled, created_at, updated_at - `, [p.id, p.name, p.description, p.priceCents, p.credits, p.sortOrder, p.enabled]); - if (!rows[0]) return NextResponse.json({ error: "套餐不存在" }, { status: 404 }); - return NextResponse.json({ package: output(rows[0]) }); - } catch (error) { - return adminErrorResponse(error); - } -} - -export async function DELETE(request: Request) { - try { - await requireAdminSession("write"); - const parsed = idSchema.safeParse(await request.json().catch(() => null)); - if (!parsed.success) return NextResponse.json({ error: "套餐参数不正确" }, { status: 400 }); - const rows = await queryAdminRows<{ id: string }>(` - update public.payment_packages - set enabled = false, updated_at = clock_timestamp() - where id = $1 - returning id - `, [parsed.data.id]); - if (!rows[0]) return NextResponse.json({ error: "套餐不存在" }, { status: 404 }); - return NextResponse.json({ ok: true }); - } catch (error) { - return adminErrorResponse(error); - } -} diff --git a/frontend/src/app/api/admin/payments/route.ts b/frontend/src/app/api/admin/payments/route.ts deleted file mode 100644 index f0399050..00000000 --- a/frontend/src/app/api/admin/payments/route.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { NextResponse } from "next/server"; -import { z } from "zod"; -import { requireAdminSession } from "@/lib/admin/auth"; -import { queryAdminRows } from "@/lib/admin/database"; -import { adminErrorResponse } from "@/lib/admin/http"; - -export const runtime = "nodejs"; - -const querySchema = z.object({ - status: z.enum(["pending", "paid", "failed", "expired"]).optional(), - from: z.string().datetime({ offset: true }).optional(), - to: z.string().datetime({ offset: true }).optional(), - limit: z.coerce.number().int().min(1).max(100).default(20), - offset: z.coerce.number().int().min(0).default(0), -}); - -type PaymentOrderRow = { - order_no: string; - user_email: string | null; - package_name: string | null; - money_cents: number; - credits: number; - status: string; - epay_trade_no: string | null; - created_at: Date; - paid_at: Date | null; - total_count: string; -}; - -type PaymentStatsRow = { - total_orders: string; - paid_orders: string; - pending_orders: string; - failed_expired_orders: string; - paid_amount_cents: string; - granted_credits: string; -}; - -export async function GET(request: Request) { - try { - await requireAdminSession("read"); - - const url = new URL(request.url); - const parsed = querySchema.safeParse(Object.fromEntries(url.searchParams)); - if (!parsed.success) return NextResponse.json({ error: "查询参数不正确" }, { status: 400 }); - const { status, from, to, limit, offset } = parsed.data; - if (from && to && new Date(from) > new Date(to)) return NextResponse.json({ error: "开始日期不能晚于结束日期" }, { status: 400 }); - - const values: unknown[] = []; - const conditions: string[] = []; - if (status) { - values.push(status); - conditions.push(`o.status = $${values.length}`); - } - if (from) { - values.push(from); - conditions.push(`o.created_at >= $${values.length}::timestamptz`); - } - if (to) { - values.push(to); - conditions.push(`o.created_at <= $${values.length}::timestamptz`); - } - const statsValues: unknown[] = []; - const dateConditions: string[] = []; - if (from) { - statsValues.push(from); - dateConditions.push(`o.created_at >= $${statsValues.length}::timestamptz`); - } - if (to) { - statsValues.push(to); - dateConditions.push(`o.created_at <= $${statsValues.length}::timestamptz`); - } - values.push(limit, offset); - - const [rows, statsRows] = await Promise.all([ - queryAdminRows(` - select - o.order_no, u.email as user_email, p.name as package_name, - o.money_cents, o.credits, o.status, o.epay_trade_no, - o.created_at, o.paid_at, count(*) over()::text as total_count - from public.payment_orders o - left join public.payment_packages p on p.id = o.package_id - left join identity.users u on u.id = o.user_id - ${conditions.length ? `where ${conditions.join(" and ")}` : ""} - order by o.created_at desc, o.order_no asc - limit $${values.length - 1} offset $${values.length} - `, values), - queryAdminRows(` - select - count(*)::text as total_orders, - count(*) filter (where o.status = 'paid')::text as paid_orders, - count(*) filter (where o.status = 'pending')::text as pending_orders, - count(*) filter (where o.status in ('failed', 'expired'))::text as failed_expired_orders, - coalesce(sum(o.money_cents) filter (where o.status = 'paid'), 0)::text as paid_amount_cents, - coalesce(sum(o.credits) filter (where o.status = 'paid'), 0)::text as granted_credits - from public.payment_orders o - ${dateConditions.length ? `where ${dateConditions.join(" and ")}` : ""} - `, statsValues), - ]); - - const orders = rows.map((row) => ({ - orderNo: row.order_no, - userEmail: row.user_email, - packageName: row.package_name, - moneyCents: row.money_cents, - credits: row.credits, - status: row.status, - epayTradeNo: row.epay_trade_no, - createdAt: row.created_at.toISOString(), - paidAt: row.paid_at?.toISOString() ?? null, - })); - const rawStats = statsRows[0]; - const stats = { - totalOrders: Number(rawStats?.total_orders ?? 0), - paidOrders: Number(rawStats?.paid_orders ?? 0), - pendingOrders: Number(rawStats?.pending_orders ?? 0), - failedExpiredOrders: Number(rawStats?.failed_expired_orders ?? 0), - paidAmountCents: Number(rawStats?.paid_amount_cents ?? 0), - grantedCredits: Number(rawStats?.granted_credits ?? 0), - }; - const total = Number(rows[0]?.total_count ?? 0); - return NextResponse.json({ orders, stats, pagination: { limit, offset, total, hasMore: offset + orders.length < total } }); - } catch (error) { - const response = adminErrorResponse(error); - if (response.status === 401 || response.status === 403) return response; - return NextResponse.json({ error: "支付记录服务暂时不可用" }, { status: 500 }); - } -} diff --git a/frontend/src/app/api/admin/session/route.ts b/frontend/src/app/api/admin/session/route.ts deleted file mode 100644 index 036a6061..00000000 --- a/frontend/src/app/api/admin/session/route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { NextResponse } from "next/server"; - -import { requireAdminSession } from "@/lib/admin/auth"; -import { adminErrorResponse } from "@/lib/admin/http"; - -export const runtime = "nodejs"; - -export async function GET() { - try { - const { user, role } = await requireAdminSession(); - return NextResponse.json({ - user: { - id: user.id, - email: user.email, - name: user.name, - role, - }, - }); - } catch (error) { - return adminErrorResponse(error); - } -} diff --git a/frontend/src/app/api/admin/users/route.ts b/frontend/src/app/api/admin/users/route.ts deleted file mode 100644 index 79d28afc..00000000 --- a/frontend/src/app/api/admin/users/route.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { NextResponse } from "next/server"; - -import { requireAdminSession } from "@/lib/admin/auth"; -import { pageOffset, queryAdminRows } from "@/lib/admin/database"; -import { - adminErrorResponse, - invalidQueryResponse, - parseListQuery, - readonlyAdminMutation, -} from "@/lib/admin/http"; - -export const runtime = "nodejs"; - -type UserRow = { - id: string; - email: string; - name: string | null; - role: string; - email_verified: boolean; - banned: boolean; - created_at: Date; - credits: number; - birth_date: string | null; - birth_time_status: string | null; - birth_place_label: string | null; - total_count: string; -}; - -const sortColumns = new Map([ - ["createdAt", "u.created_at"], - ["email", "u.email"], - ["credits", "p.credits"], - ["name", "u.name"], -]); - -export const POST = readonlyAdminMutation; -export const PUT = readonlyAdminMutation; -export const PATCH = readonlyAdminMutation; -export const DELETE = readonlyAdminMutation; - -export async function GET(request: Request) { - try { - await requireAdminSession(); - const parsed = parseListQuery(request); - if (!parsed.success) return invalidQueryResponse(parsed.error.flatten()); - const { page, pageSize, sort, order, q } = parsed.data; - const values: unknown[] = []; - const conditions: string[] = []; - if (q) { - values.push(`%${q}%`); - conditions.push(`(u.email ilike $${values.length} or u.name ilike $${values.length})`); - } - values.push(pageSize, pageOffset(page, pageSize)); - const sortColumn = sortColumns.get(sort ?? "createdAt") ?? "u.created_at"; - const rows = await queryAdminRows(` - select - u.id, u.email, u.name, u.role, u.email_verified, u.banned, - u.created_at, p.credits, p.birth_date, p.birth_time_status, - p.birth_place_label, count(*) over()::text as total_count - from identity.users u - join public.profiles p on p.id = u.id - ${conditions.length ? `where ${conditions.join(" and ")}` : ""} - order by ${sortColumn} ${order === "asc" ? "asc" : "desc"}, u.id asc - limit $${values.length - 1} offset $${values.length} - `, values); - return NextResponse.json({ - data: rows.map((row) => ({ - id: row.id, - email: row.email, - name: row.name, - role: row.role, - emailVerified: row.email_verified, - banned: row.banned, - createdAt: row.created_at.toISOString(), - credits: row.credits, - birthDate: row.birth_date, - birthTimeStatus: row.birth_time_status, - birthPlace: row.birth_place_label, - })), - total: Number(rows[0]?.total_count ?? 0), - }); - } catch (error) { - return adminErrorResponse(error); - } -} diff --git a/frontend/src/app/api/auth/[...all]/route.ts b/frontend/src/app/api/auth/[...all]/route.ts index 21755f8f..2a25646d 100644 --- a/frontend/src/app/api/auth/[...all]/route.ts +++ b/frontend/src/app/api/auth/[...all]/route.ts @@ -24,6 +24,7 @@ async function dispatch( const services = getIdentityAuthServices(); const handlers = createHostIsolatedAuthHandlers(config, { user: toNextJsHandler(services.user), + admin: toNextJsHandler(services.admin), }); return handlers[method](request); } diff --git a/frontend/src/app/api/payment/epay/create/route.ts b/frontend/src/app/api/payment/epay/create/route.ts deleted file mode 100644 index 9686accb..00000000 --- a/frontend/src/app/api/payment/epay/create/route.ts +++ /dev/null @@ -1,53 +0,0 @@ -import crypto from "node:crypto"; -import { NextResponse } from "next/server"; -import { z } from "zod"; -import { createAdminSupabaseClient } from "@/lib/supabase/admin"; -import { createServerSupabaseClient } from "@/lib/supabase/server"; -import { epaySign } from "@/lib/epay/sign"; -import { readEpayAvailability } from "@/lib/epay/availability"; -import { epaySubmitUrl, readEpayConfig, EpayConfigurationError } from "@/lib/epay/config"; -import { assertPublicGatewayUrl } from "@/lib/epay/gateway-policy"; - -export const runtime = "nodejs"; -const schema = z.object({ packageId: z.string().uuid() }); - -export async function POST(request: Request) { - try { - const client = await createServerSupabaseClient(); - const { data: { user } } = await client.auth.getUser(); - if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 }); - const parsed = schema.safeParse(await request.json().catch(() => null)); - if (!parsed.success) return NextResponse.json({ error: "请选择有效套餐" }, { status: 400 }); - - const availability = await readEpayAvailability(); - if (!availability.enabled) return NextResponse.json({ error: "在线支付暂未开放", code: "EPAY_DISABLED" }, { status: 403 }); - const config = await readEpayConfig(); - const submitUrl = epaySubmitUrl(config.gatewayUrl); - await assertPublicGatewayUrl(submitUrl); - - const admin = createAdminSupabaseClient(); - const { data: pack, error: packError } = await admin.from("payment_packages").select("id,name,price_cents,credits,enabled").eq("id", parsed.data.packageId).eq("enabled", true).maybeSingle(); - if (packError || !pack) return NextResponse.json({ error: "套餐不存在或已下架" }, { status: 404 }); - const orderNo = `JY${Date.now().toString(36)}${crypto.randomBytes(10).toString("hex")}`; - const { error: orderError } = await admin.from("payment_orders").insert({ order_no: orderNo, user_id: user.id, package_id: pack.id, money_cents: pack.price_cents, credits: pack.credits }); - if (orderError) return NextResponse.json({ error: "创建订单失败" }, { status: 500 }); - - const params = { - money: (pack.price_cents / 100).toFixed(2), - name: pack.name, - notify_url: config.notifyUrl, - out_trade_no: orderNo, - pid: config.pid, - return_url: config.returnUrl, - sitename: config.siteName, - type: "alipay", - }; - const signedParams = { ...params, sign: epaySign(params, config.key), sign_type: "MD5" }; - const payUrl = new URL(submitUrl); - for (const [name, value] of Object.entries(signedParams)) payUrl.searchParams.set(name, value); - return NextResponse.json({ orderNo, payUrl: payUrl.toString(), qrCode: null }); - } catch (error) { - if (error instanceof EpayConfigurationError) return NextResponse.json({ error: "易支付尚未配置", code: "EPAY_NOT_CONFIGURED" }, { status: 503 }); - return NextResponse.json({ error: "创建支付失败" }, { status: 500 }); - } -} diff --git a/frontend/src/app/api/payment/epay/notify/route.ts b/frontend/src/app/api/payment/epay/notify/route.ts deleted file mode 100644 index 7c3b7523..00000000 --- a/frontend/src/app/api/payment/epay/notify/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import crypto from "node:crypto"; -import { NextResponse } from "next/server"; -import { createAdminSupabaseClient } from "@/lib/supabase/admin"; -import { epaySign, timingSafeSignEqual } from "@/lib/epay/sign"; -import { readEpayConfig } from "@/lib/epay/config"; -export const runtime = "nodejs"; -async function notify(request: Request) { - try { - const config = await readEpayConfig(); const raw = request.method === "GET" ? new URL(request.url).search.slice(1) : await request.text(); const params = new URLSearchParams(raw); const values: Record = {}; params.forEach((value, key) => { values[key] = value; }); - if (!timingSafeSignEqual(values.sign, epaySign(values, config.key)) || values.pid !== config.pid || values.trade_status !== "TRADE_SUCCESS" || !values.out_trade_no || !values.money) return new NextResponse("success", { status: 200 }); - const moneyCents = Math.round(Number(values.money) * 100); if (!Number.isSafeInteger(moneyCents) || moneyCents <= 0) return new NextResponse("success", { status: 200 }); - const hash = crypto.createHash("sha256").update(raw).digest("hex"); - const { error } = await createAdminSupabaseClient().rpc("settle_epay_order", { p_order_no: values.out_trade_no, p_trade_no: values.trade_no || values.transaction_id || values.out_trade_no, p_money_cents: moneyCents, p_payload_hash: hash }); - if (error) return new NextResponse("success", { status: 200 }); - return new NextResponse("success", { status: 200 }); - } catch { return new NextResponse("success", { status: 200 }); } -} -export async function POST(request: Request) { return notify(request); } -export async function GET(request: Request) { return notify(request); } diff --git a/frontend/src/app/api/payment/epay/status/route.ts b/frontend/src/app/api/payment/epay/status/route.ts deleted file mode 100644 index a1451a94..00000000 --- a/frontend/src/app/api/payment/epay/status/route.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { NextResponse } from "next/server"; -import { createAdminSupabaseClient } from "@/lib/supabase/admin"; -import { createServerSupabaseClient } from "@/lib/supabase/server"; -export const runtime = "nodejs"; -export async function GET(request: Request) { const client = await createServerSupabaseClient(); const { data: { user } } = await client.auth.getUser(); if (!user) return NextResponse.json({ error: "请先登录" }, { status: 401 }); const orderNo = new URL(request.url).searchParams.get("orderNo"); if (!orderNo) return NextResponse.json({ error: "缺少订单号" }, { status: 400 }); const { data, error } = await createAdminSupabaseClient().from("payment_orders").select("order_no,status,credits,paid_at").eq("order_no", orderNo).eq("user_id", user.id).maybeSingle(); if (error) return NextResponse.json({ error: "暂时无法查询订单" }, { status: 500 }); if (!data) return NextResponse.json({ error: "订单不存在" }, { status: 404 }); return NextResponse.json({ orderNo: data.order_no, status: data.status, credits: data.credits, paidAt: data.paid_at }); } diff --git a/frontend/src/app/api/payment/packages/route.ts b/frontend/src/app/api/payment/packages/route.ts deleted file mode 100644 index 062128af..00000000 --- a/frontend/src/app/api/payment/packages/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NextResponse } from "next/server"; -import { readEpayAvailability } from "@/lib/epay/availability"; -import { createAdminSupabaseClient } from "@/lib/supabase/admin"; - -export const runtime = "nodejs"; - -export async function GET() { - const availability = await readEpayAvailability(); - if (!availability.enabled) return NextResponse.json({ enabled: false, packages: [] }); - - const { data, error } = await createAdminSupabaseClient() - .from("payment_packages") - .select("id,name,description,price_cents,credits,sort_order") - .eq("enabled", true) - .order("sort_order") - .order("created_at"); - if (error) return NextResponse.json({ enabled: false, packages: [] }); - return NextResponse.json({ - enabled: true, - packages: (data || []).map((item) => ({ - id: item.id, - name: item.name, - description: item.description, - priceCents: item.price_cents, - credits: item.credits, - })), - }); -} diff --git a/frontend/src/app/api/rectification/v4/cases/[caseId]/events/[eventId]/revisions/route.ts b/frontend/src/app/api/rectification/v4/cases/[caseId]/events/[eventId]/revisions/route.ts index 31955439..23690572 100644 --- a/frontend/src/app/api/rectification/v4/cases/[caseId]/events/[eventId]/revisions/route.ts +++ b/frontend/src/app/api/rectification/v4/cases/[caseId]/events/[eventId]/revisions/route.ts @@ -18,8 +18,6 @@ export async function POST(request: Request, { params }: { params: Promise<{ cas eventId, domain: body.domain, eventKind: body.eventKind, - subject: body.subject, - relatedPerson: body.relatedPerson, summary: body.summary, rawText: body.rawText, dateRange: body.dateRange, diff --git a/frontend/src/app/api/redeem/route.ts b/frontend/src/app/api/redeem/route.ts index c4e9227e..506a9df4 100644 --- a/frontend/src/app/api/redeem/route.ts +++ b/frontend/src/app/api/redeem/route.ts @@ -12,7 +12,6 @@ const redeemErrors: Record = { unauthorized: { status: 401, message: "请先登录" }, invalid_code: { status: 404, message: "兑换码不存在" }, expired_code: { status: 410, message: "兑换码已过期" }, - revoked_code: { status: 410, message: "兑换码已撤销" }, already_redeemed: { status: 409, message: "兑换码已被使用" }, profile_missing: { status: 500, message: "账户资料不存在,请稍后重试" }, }; diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 853985d8..e665c9a2 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -367,13 +367,7 @@ button:disabled { cursor: default; opacity: .45; } .chat-header span { color: var(--color-ink-secondary); margin-top: var(--space-1); font-size: 12px; } .status { display: inline-block; margin: 0 6px 1px 0; border-radius: 50%; background: var(--color-success); width: 7px; height: 7px; margin-right: var(--space-2); } .status-loading { background: var(--color-action); } -.chat-header > .chat-header-actions { min-width: auto; display: flex; flex: 0 0 auto; align-items: center; gap: var(--space-2); } -.admin-button, .credit-button { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas-soft); color: var(--color-ink-secondary); transition: background-color 120ms ease-out, border-color 120ms ease-out, color 120ms ease-out, transform 120ms ease-out; } -.admin-button { width: 44px; flex: 0 0 44px; } -.admin-button svg { width: 18px; height: 18px; } -.admin-button:hover, .credit-button:hover { border-color: color-mix(in srgb, var(--color-action) 35%, var(--color-border)); background: var(--color-action-soft); color: var(--color-action-hover); } -.admin-button:active, .credit-button:active { transform: translateY(1px); } -.credit-button { gap: 6px; min-width: 64px; padding: 0 11px; cursor: pointer; font-size: 13px; font-variant-numeric: tabular-nums; font-weight: 500; } +.credit-button { min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 11px; cursor: pointer; font-size: 13px; font-variant-numeric: tabular-nums; transition: background-color 120ms ease-out, transform 120ms ease-out; min-width: 64px; border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas-soft); color: var(--color-ink-secondary); font-weight: 500; } .conversation { min-width: 0; min-height: 0; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable; padding-bottom: var(--composer-reserve); background: var(--color-canvas); } .conversation.is-empty { display: grid; place-items: center; padding: var(--space-8); } @@ -744,8 +738,6 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background: .auth-links { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--space-2); } .auth-links button { min-height: 32px; padding: 0; color: var(--color-action); } -.admin-app-shell { height: 100dvh; min-height: 0; overflow-y: auto; } -.admin-app-shell > *, .admin-app-shell .ant-layout { min-height: 100%; } .admin-page { background: var(--color-canvas-soft); } .admin-header { position: sticky; z-index: 4; top: 0; display: flex; align-items: center; justify-content: space-between; gap: 20px; border-bottom: 1px solid var(--color-border); min-height: 88px; padding: 0 var(--space-8); background: var(--color-frosted); backdrop-filter: saturate(130%) blur(20px); } .admin-header h1 { font-size: var(--type-display-md); } @@ -763,14 +755,6 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background: .code-status { display: inline-flex; min-height: 28px; align-items: center; padding: 0 9px; border-radius: var(--radius-md); background: var(--color-canvas-muted); color: var(--color-ink-secondary); } .status-可用 { background: var(--color-success-muted); color: var(--color-success); } .status-已过期, .status-已兑换, .empty-cell { color: var(--color-ink-tertiary); } -.payment-stats { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 1px; margin-top: 16px; overflow: hidden; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-border); } -.payment-stats > div { display: grid; gap: 6px; padding: var(--space-4); background: var(--color-canvas-muted); } -.payment-stats span, .payment-filters label span { color: var(--color-ink-secondary); font-size: 12px; } -.payment-stats strong { font-size: 20px; font-variant-numeric: tabular-nums; } -.payment-filters { display: flex; flex-wrap: wrap; gap: var(--space-4); margin-top: 16px; } -.payment-filters label { display: grid; gap: 6px; } -.payment-pagination { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-3); margin-top: 16px; color: var(--color-ink-secondary); font-size: 13px; } -@media (max-width: 767px) { .payment-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } .payment-pagination { justify-content: space-between; } } @media (hover: hover) { .new-chat:not(:disabled):hover { background: var(--color-surface-dark-raised); } @@ -1667,9 +1651,3 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background: .birth-time-clock-menu.select-content { width: 108px; min-width: 108px; } .birth-time-clock-menu .select-item { justify-content: flex-start; } - -.payment-qr-wrap { position: relative; width: min(220px, 72vw); aspect-ratio: 1; margin: 14px auto; padding: 10px; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: #fff; box-shadow: var(--shadow-elevated); } -.payment-qr-wrap img { display: block; width: 100%; height: 100%; object-fit: contain; } -.payment-qr-badge { position: absolute; top: 50%; left: 50%; display: grid; width: 44px; height: 44px; padding: 4px; transform: translate(-50%, -50%); border: 4px solid #fff; border-radius: 12px; background: #fff; box-shadow: 0 2px 10px rgb(0 0 0 / 18%); } -.payment-qr-badge svg { display: block; width: 100%; height: 100%; } - diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx index 63196fc2..38692169 100644 --- a/frontend/src/app/login/page.tsx +++ b/frontend/src/app/login/page.tsx @@ -1,14 +1,35 @@ +import { headers } from "next/headers"; + import { EmailOtpLogin } from "@/components/email-otp-login"; -import { readIdentityConfig } from "@/modules/identity/config"; +import { + isSelfHostedIdentityEnabled, + readIdentityConfig, + readSelfHostedIdentityConfig, +} from "@/modules/identity/config"; +import { resolveIdentitySurface } from "@/modules/identity/host"; export const dynamic = "force-dynamic"; export default async function LoginPage() { const config = readIdentityConfig(process.env); + let provider = config.provider; + let passwordEnabled = false; + let passwordOnly = false; + if (isSelfHostedIdentityEnabled(process.env)) { + const selfHosted = readSelfHostedIdentityConfig(process.env); + const surface = resolveIdentitySurface( + (await headers()).get("host"), + selfHosted, + ); + if (surface === "admin") provider = "self-hosted"; + passwordEnabled = provider === "self-hosted"; + passwordOnly = surface === "admin"; + } return ( ); } diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 9309c3cc..18f7f1fa 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import dynamic from "next/dynamic"; -import { ArrowUp, ArrowUpRight, ShieldCheck, Sparkles, Square, X } from "lucide-react"; +import { ArrowUp, ArrowUpRight, Sparkles, Square, X } from "lucide-react"; import { useGSAP } from "@gsap/react"; import { gsap } from "gsap"; import { useEffect, useRef, useState } from "react"; @@ -180,7 +180,6 @@ type Account = { user: { id: string; email: string | null }; credits: number; isAdmin: boolean; - adminUrl: string | null; rectificationPriceCredits: number; hasConfirmedBirthTime: boolean; rectificationCase: AccountRectificationCaseState | null; @@ -925,11 +924,6 @@ export default function Home() { const [redeemError, setRedeemError] = useState(""); const [redeemMessage, setRedeemMessage] = useState(""); const [redeeming, setRedeeming] = useState(false); - const [paymentEnabled, setPaymentEnabled] = useState(false); - const [paymentPackages, setPaymentPackages] = useState>([]); - const [paymentOrder, setPaymentOrder] = useState<{ orderNo: string; payUrl: string | null; qrCode: string | null; status: string } | null>(null); - const [paymentError, setPaymentError] = useState(""); - const [payingPackageId, setPayingPackageId] = useState(null); const [signingOut, setSigningOut] = useState(false); const [sessions, setSessions] = useState([]); const [pinnedSessionIds, setPinnedSessionIds] = useState([]); @@ -1245,7 +1239,6 @@ export default function Home() { user: { id: "preview-user", email: "preview@local.test" }, credits: 8, isAdmin: false, - adminUrl: null, rectificationPriceCredits: 1, hasConfirmedBirthTime: previewProfile.birthTimeStatus === "confirmed", rectificationCase: null, @@ -1676,10 +1669,6 @@ export default function Home() { case "redeem": setRedeemError(""); setRedeemMessage(""); - setPaymentEnabled(false); - setPaymentPackages([]); - setPaymentOrder(null); - setPaymentError(""); break; case "logout": break; @@ -1952,47 +1941,6 @@ export default function Home() { } } - useEffect(() => { - if (activeAccountDialog !== "redeem") return; - void fetch("/api/payment/packages", { cache: "no-store" }).then(async (response) => { - const payload = await response.json().catch(() => null); - if (response.ok && payload?.enabled === true) { - setPaymentEnabled(true); - setPaymentPackages(payload.packages || []); - return; - } - if (!response.ok) setPaymentError("套餐支付暂时不可用,请稍后重试"); - }).catch(() => { - setPaymentError("套餐支付暂时不可用,请稍后重试"); - }); - }, [activeAccountDialog]); - - useEffect(() => { - if (!paymentOrder || paymentOrder.status === "paid") return; - const timer = window.setInterval(() => { - void fetch(`/api/payment/epay/status?orderNo=${encodeURIComponent(paymentOrder.orderNo)}`, { cache: "no-store" }).then(async (response) => { - const payload = await response.json().catch(() => null); - if (!response.ok) return; - setPaymentOrder((current) => current ? { ...current, status: payload.status } : current); - if (payload.status === "paid") void refreshAccount(); - }); - }, 3000); - return () => window.clearInterval(timer); - }, [paymentOrder]); - - async function createPayment(packageId: string) { - if (payingPackageId) return; - setPayingPackageId(packageId); setPaymentError(""); - try { - const response = await fetch("/api/payment/epay/create", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ packageId }) }); - const payload = await response.json().catch(() => null); - if (!response.ok) throw new Error(payload?.error || "创建支付失败"); - if (typeof payload?.orderNo !== "string" || typeof payload?.payUrl !== "string") throw new Error("创建支付失败"); - setPaymentOrder({ orderNo: payload.orderNo, payUrl: payload.payUrl, qrCode: payload.qrCode ?? null, status: "pending" }); - window.open(payload.payUrl, "_blank", "noopener,noreferrer"); - } catch (caught) { setPaymentError(caught instanceof Error ? caught.message : "创建支付失败"); } finally { setPayingPackageId(null); } - } - async function redeem(event: FormEvent) { event.preventDefault(); const code = redeemCode.trim(); @@ -2804,7 +2752,6 @@ export default function Home() { email: account.user.email || "尚未读取邮箱", credits: account.credits, isAdmin: account.isAdmin, - adminUrl: account.adminUrl, initial: profile.name.trim().slice(0, 1) || account.user.email?.slice(0, 1).toUpperCase() || "你", @@ -2888,17 +2835,10 @@ export default function Home() { ? "正在校正出生时间" : personalChartAvailable ? "基于星盘证据回答" : "回答一般占星知识"} -
- {account.isAdmin && account.adminUrl ? ( - -
+ {!rectificationSurfaceOpen && ( @@ -3282,12 +3222,6 @@ export default function Home() { {redeemError &&

{redeemError}

} {redeemMessage &&

{redeemMessage}

} - {paymentEnabled &&
-

套餐充值

- {paymentPackages.map((item) =>
{item.name}{item.description || `${item.credits} 点`}
¥{(item.priceCents / 100).toFixed(2)}
)} - {paymentOrder &&

订单 {paymentOrder.orderNo}:{paymentOrder.status === "paid" ? "支付成功,点数已到账" : "等待支付"}

} -
} - {paymentError &&

{paymentError}

} )} diff --git a/frontend/src/components/admin/admin-app.tsx b/frontend/src/components/admin/admin-app.tsx deleted file mode 100644 index 045daa24..00000000 --- a/frontend/src/components/admin/admin-app.tsx +++ /dev/null @@ -1,81 +0,0 @@ -"use client"; - -import { - ArrowLeftOutlined, - AuditOutlined, - CreditCardOutlined, - GiftOutlined, - ShoppingOutlined, - MessageOutlined, - TeamOutlined, - TransactionOutlined, -} from "@ant-design/icons"; -import { Authenticated, Refine } from "@refinedev/core"; -import { ErrorComponent, ThemedLayout, ThemedSider, useNotificationProvider } from "@refinedev/antd"; -import routerProvider from "@refinedev/nextjs-router"; -import { App as AntdApp, ConfigProvider, Menu, Spin, theme } from "antd"; -import Link from "next/link"; -import type { ReactNode } from "react"; - -import { - adminAccessControlProvider, - adminAuthProvider, - adminDataProvider, -} from "@/lib/admin/providers"; - -function AdminSider() { - return ( - ( - <> - {items} - } title="返回对话"> - {collapsed ? null : "返回对话"} - - - )} - /> - ); -} - -export function AdminApp({ children }: { children: ReactNode }) { - const notificationProvider = useNotificationProvider(); - return ( -
- - - } }, - { name: "payments", list: "/admin/payments", meta: { label: "支付管理", icon: } }, - { name: "packages", list: "/admin/packages", meta: { label: "套餐管理", icon: } }, - { name: "users", list: "/admin/codes?resource=users", meta: { label: "用户资料", icon: } }, - { name: "credit-transactions", list: "/admin/codes?resource=credit-transactions", meta: { label: "积分流水", icon: } }, - { name: "consultations", list: "/admin/codes?resource=consultations", meta: { label: "咨询请求", icon: } }, - { name: "audit-logs", list: "/admin/codes?resource=audit-logs", meta: { label: "审计日志", icon: } }, - ]} - options={{ - syncWithLocation: true, - warnWhenUnsavedChanges: true, - title: { text: "Jyotisha 后台" }, - }} - > - 正在验证后台权限
} - > - {children} - - - - - - ); -} - -export { ErrorComponent as AdminErrorComponent }; diff --git a/frontend/src/components/admin/audit-logs-resource.tsx b/frontend/src/components/admin/audit-logs-resource.tsx deleted file mode 100644 index 0b7479ad..00000000 --- a/frontend/src/components/admin/audit-logs-resource.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; - -import { Descriptions, Tag, type TableColumnsType } from "antd"; - -import { formatAdminDate, ResourceTable } from "@/components/admin/resource-table"; - -type AuditRecord = { - id: string; - actorEmail: string; - actorRole: string; - action: string; - targetId: string; - before: Record | null; - after: Record | null; - requestId: string; - createdAt: string; -}; - -const columns: TableColumnsType = [ - { title: "操作者", dataIndex: "actorEmail", sorter: true }, - { title: "角色", dataIndex: "actorRole", render: (value) => {value} }, - { title: "动作", dataIndex: "action", sorter: true }, - { title: "目标 ID", dataIndex: "targetId" }, - { title: "Request ID", dataIndex: "requestId" }, - { title: "时间", dataIndex: "createdAt", sorter: true, render: formatAdminDate }, -]; - -export default function AuditLogsPage() { - return - resource="audit-logs" - title="审计日志(只读)" - columns={columns} - statusOptions={[ - { label: "生成兑换码", value: "redemption_code.create" }, - { label: "修改兑换码", value: "redemption_code.update" }, - { label: "撤销兑换码", value: "redemption_code.revoke" }, - ]} - extra={} - />; -} diff --git a/frontend/src/components/admin/codes-resource.tsx b/frontend/src/components/admin/codes-resource.tsx deleted file mode 100644 index 28e14515..00000000 --- a/frontend/src/components/admin/codes-resource.tsx +++ /dev/null @@ -1,167 +0,0 @@ -"use client"; - -import { useCreate, useDelete, useGetIdentity, usePermissions, useUpdate } from "@refinedev/core"; -import { Button, DatePicker, Form, Input, InputNumber, Modal, Space, Tag, Typography, type TableColumnsType } from "antd"; -import dayjs from "dayjs"; -import { useState } from "react"; - -import { formatAdminDate, ResourceTable } from "@/components/admin/resource-table"; -import type { AdminIdentity } from "@/lib/admin/providers"; - -type CodeRecord = { - id: string; - code?: string; - mask: string; - credits: number; - expiresAt: string | null; - note: string | null; - createdAt: string; - redeemedEmail: string | null; - redeemedAt: string | null; - revokedAt: string | null; - status: "available" | "expired" | "redeemed" | "revoked"; -}; - -type CreateValues = { - credits: number; - count: number; - expiresAt?: ReturnType; - note?: string; -}; -type EditValues = { note?: string; expiresAt?: ReturnType | null }; - -const statusColors: Record = { - available: "green", - expired: "orange", - redeemed: "blue", - revoked: "red", -}; - -export default function CodesPage() { - const { data: role } = usePermissions<"admin">({}); - const { data: identity } = useGetIdentity(); - const { mutate: createCodes, mutation: createMutation } = useCreate<{ id: string; generated: CodeRecord[] }>(); - const { mutate: updateCode, mutation: updateMutation } = useUpdate(); - const { mutate: revokeCode, mutation: revokeMutation } = useDelete(); - const [createOpen, setCreateOpen] = useState(false); - const [editRecord, setEditRecord] = useState(null); - const [generated, setGenerated] = useState([]); - const [createForm] = Form.useForm(); - const [editForm] = Form.useForm(); - const writable = role === "admin"; - - function submitCreate(values: CreateValues) { - createCodes({ - resource: "codes", - values: { - credits: values.credits, - count: values.count, - expiresAt: values.expiresAt?.toISOString() ?? null, - note: values.note?.trim() || null, - }, - successNotification: false, - }, { - onSuccess(result) { - setGenerated(result.data.generated); - setCreateOpen(false); - createForm.resetFields(); - }, - }); - } - - function submitEdit(values: EditValues) { - if (!editRecord) return; - updateCode({ - resource: "codes", - id: editRecord.id, - values: { - note: values.note?.trim() || null, - expiresAt: values.expiresAt?.toISOString() ?? null, - }, - }, { onSuccess: () => setEditRecord(null) }); - } - - function confirmRevoke(record: CodeRecord) { - Modal.confirm({ - title: "撤销此兑换码?", - content: `${record.mask} 撤销后不可兑换,且不能恢复。`, - okText: "确认撤销", - okButtonProps: { danger: true }, - cancelText: "取消", - onOk: () => new Promise((resolve, reject) => { - revokeCode({ resource: "codes", id: record.id }, { - onSuccess: () => resolve(), - onError: () => reject(new Error("撤销失败")), - }); - }), - }); - } - - const columns: TableColumnsType = [ - { title: "兑换码", dataIndex: "mask" }, - { title: "点数", dataIndex: "credits", sorter: true }, - { title: "状态", dataIndex: "status", sorter: true, render: (value) => {value} }, - { title: "到期时间", dataIndex: "expiresAt", sorter: true, render: formatAdminDate }, - { title: "备注", dataIndex: "note", render: (value) => value || "—" }, - { title: "兑换账户", dataIndex: "redeemedEmail", render: (value) => value || "—" }, - { title: "兑换时间", dataIndex: "redeemedAt", render: formatAdminDate }, - { title: "撤销时间", dataIndex: "revokedAt", render: formatAdminDate }, - { title: "创建时间", dataIndex: "createdAt", sorter: true, render: formatAdminDate }, - { - title: "操作", - fixed: "right", - render: (_, record) => writable && record.status !== "redeemed" && record.status !== "revoked" ? ( - - - - - ) : "—", - }, - ]; - - return ( - <> - - resource="codes" - title={`兑换码${identity ? ` · ${identity.email} (${identity.role})` : ""}`} - columns={columns} - statusOptions={[ - { label: "可用", value: "available" }, - { label: "已过期", value: "expired" }, - { label: "已兑换", value: "redeemed" }, - { label: "已撤销", value: "revoked" }, - ]} - extra={writable ? : null} - /> - - setCreateOpen(false)} footer={null} destroyOnHidden> -
- - - - - -
-
- - 0} onCancel={() => setGenerated([])} footer={}> - 关闭后无法再次查看完整兑换码,请立即安全保存。 - {generated.map((record) => {record.code})} - - - setEditRecord(null)} footer={null} destroyOnHidden> -
- - - -
-
- - ); -} diff --git a/frontend/src/components/admin/consultations-resource.tsx b/frontend/src/components/admin/consultations-resource.tsx deleted file mode 100644 index fda31052..00000000 --- a/frontend/src/components/admin/consultations-resource.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; - -import { Tag, type TableColumnsType } from "antd"; - -import { formatAdminDate, ResourceTable } from "@/components/admin/resource-table"; - -type ConsultationRecord = { - id: string; - email: string | null; - requestId: string; - status: string; - createdAt: string; - updatedAt: string; -}; - -const colors: Record = { - reserved: "gold", - completed: "green", - cancelled: "default", -}; -const columns: TableColumnsType = [ - { title: "用户", dataIndex: "email", render: (value) => value || "—" }, - { title: "请求 ID", dataIndex: "requestId" }, - { title: "状态", dataIndex: "status", sorter: true, render: (value) => {value} }, - { title: "创建时间", dataIndex: "createdAt", sorter: true, render: formatAdminDate }, - { title: "更新时间", dataIndex: "updatedAt", sorter: true, render: formatAdminDate }, -]; - -export default function ConsultationsPage() { - return - resource="consultations" - title="咨询请求(只读)" - columns={columns} - statusOptions={[ - { label: "已预扣", value: "reserved" }, - { label: "已完成", value: "completed" }, - { label: "已取消", value: "cancelled" }, - ]} - />; -} diff --git a/frontend/src/components/admin/credit-transactions-resource.tsx b/frontend/src/components/admin/credit-transactions-resource.tsx deleted file mode 100644 index 4e1ebff7..00000000 --- a/frontend/src/components/admin/credit-transactions-resource.tsx +++ /dev/null @@ -1,42 +0,0 @@ -"use client"; - -import { Tag, type TableColumnsType } from "antd"; - -import { formatAdminDate, ResourceTable } from "@/components/admin/resource-table"; - -type TransactionRecord = { - id: string; - email: string | null; - type: string; - amount: number; - balanceAfter: number; - requestId: string; - model: string | null; - inputTokens: number | null; - outputTokens: number | null; - createdAt: string; -}; - -const columns: TableColumnsType = [ - { title: "用户", dataIndex: "email", render: (value) => value || "—" }, - { title: "类型", dataIndex: "type", sorter: true, render: (value) => {value} }, - { title: "变动", dataIndex: "amount", sorter: true, render: (value) => value > 0 ? `+${value}` : value }, - { title: "余额", dataIndex: "balanceAfter", sorter: true }, - { title: "请求 ID", dataIndex: "requestId" }, - { title: "模型", dataIndex: "model", render: (value) => value || "—" }, - { title: "输入/输出 token", render: (_, row) => `${row.inputTokens ?? "—"} / ${row.outputTokens ?? "—"}` }, - { title: "时间", dataIndex: "createdAt", sorter: true, render: formatAdminDate }, -]; - -export default function CreditTransactionsPage() { - return - resource="credit-transactions" - title="积分流水(只读)" - columns={columns} - statusOptions={[ - { label: "兑换", value: "redeem" }, - { label: "预扣", value: "reserve" }, - { label: "退款", value: "refund" }, - ]} - />; -} diff --git a/frontend/src/components/admin/package-management.tsx b/frontend/src/components/admin/package-management.tsx deleted file mode 100644 index b77d2731..00000000 --- a/frontend/src/components/admin/package-management.tsx +++ /dev/null @@ -1,163 +0,0 @@ -"use client"; - -import { PlusOutlined } from "@ant-design/icons"; -import { List } from "@refinedev/antd"; -import { Alert, App, Button, Card, Form, Input, InputNumber, Modal, Popconfirm, Row, Col, Space, Switch, Table, Tag, Typography, type TableColumnsType } from "antd"; -import { useCallback, useEffect, useState } from "react"; - -const { Text } = Typography; - -type PaymentPackage = { - id: string; - name: string; - description: string; - priceCents: number; - credits: number; - sortOrder: number; - enabled: boolean; -}; - -type PackageFormValues = Omit & { priceYuan: number }; - -function formatMoney(cents: number) { - return `¥${(cents / 100).toFixed(2)}`; -} - -async function responsePayload(response: Response) { - const payload = await response.json().catch(() => ({})); - if (!response.ok) throw new Error(payload.error || "请求失败"); - return payload; -} - -export function PackageManagement() { - const { message } = App.useApp(); - const [packageForm] = Form.useForm(); - const [packages, setPackages] = useState([]); - const [packagesLoading, setPackagesLoading] = useState(true); - const [packagesError, setPackagesError] = useState(""); - const [modalOpen, setModalOpen] = useState(false); - const [editingPackage, setEditingPackage] = useState(null); - const [saving, setSaving] = useState(false); - const [disablingId, setDisablingId] = useState(null); - - const loadPackages = useCallback(async () => { - setPackagesLoading(true); - setPackagesError(""); - try { - const payload = await responsePayload(await fetch("/api/admin/packages", { cache: "no-store" })); - setPackages(payload.packages); - } catch (error) { - setPackagesError(error instanceof Error ? error.message : "读取套餐失败"); - } finally { - setPackagesLoading(false); - } - }, []); - - useEffect(() => { - const timer = window.setTimeout(() => void loadPackages(), 0); - return () => window.clearTimeout(timer); - }, [loadPackages]); - - function openCreateModal() { - setEditingPackage(null); - packageForm.setFieldsValue({ name: "", description: "", priceYuan: 1, credits: 10, sortOrder: 0, enabled: true }); - setModalOpen(true); - } - - function openEditModal(item: PaymentPackage) { - setEditingPackage(item); - packageForm.setFieldsValue({ - name: item.name, - description: item.description, - priceYuan: item.priceCents / 100, - credits: item.credits, - sortOrder: item.sortOrder, - enabled: item.enabled, - }); - setModalOpen(true); - } - - function closeModal() { - if (saving) return; - setModalOpen(false); - setEditingPackage(null); - packageForm.resetFields(); - } - - async function savePackage(values: PackageFormValues) { - setSaving(true); - try { - await responsePayload(await fetch("/api/admin/packages", { - method: editingPackage ? "PATCH" : "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - ...(editingPackage ? { id: editingPackage.id } : {}), - name: values.name.trim(), - description: values.description?.trim() ?? "", - priceCents: Math.round(values.priceYuan * 100), - credits: values.credits, - sortOrder: values.sortOrder, - enabled: values.enabled, - }), - })); - message.success(editingPackage ? "套餐已更新" : "套餐已添加"); - setModalOpen(false); - setEditingPackage(null); - packageForm.resetFields(); - await loadPackages(); - } catch (error) { - message.error(error instanceof Error ? error.message : "保存套餐失败"); - } finally { - setSaving(false); - } - } - - async function disablePackage(id: string) { - setDisablingId(id); - try { - await responsePayload(await fetch("/api/admin/packages", { - method: "DELETE", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ id }), - })); - message.success("套餐已停用"); - await loadPackages(); - } catch (error) { - message.error(error instanceof Error ? error.message : "停用套餐失败"); - } finally { - setDisablingId(null); - } - } - - const columns: TableColumnsType = [ - { title: "名称", dataIndex: "name", render: (_, item) => {item.name}{item.description || "暂无描述"} }, - { title: "价格", dataIndex: "priceCents", align: "right", render: formatMoney }, - { title: "点数", dataIndex: "credits", align: "right" }, - { title: "排序", dataIndex: "sortOrder", align: "right" }, - { title: "状态", dataIndex: "enabled", render: (enabled) => {enabled ? "启用" : "停用"} }, - { title: "操作", key: "actions", fixed: "right", render: (_, item) => {item.enabled && disablePackage(item.id)}>} }, - ]; - - return ( - - } onClick={openCreateModal}>添加套餐}> - - {packagesError && void loadPackages()}>重试} />} - rowKey="id" columns={columns} dataSource={packages} loading={packagesLoading} pagination={false} scroll={{ x: "max-content" }} /> - - - packageForm.submit()} onCancel={closeModal} destroyOnHidden afterClose={() => packageForm.resetFields()} maskClosable={!saving} keyboard={!saving}> - form={packageForm} layout="vertical" onFinish={savePackage} requiredMark="optional" initialValues={{ priceYuan: 1, credits: 10, sortOrder: 0, enabled: true }}> - - - - - - - - - - - - ); -} diff --git a/frontend/src/components/admin/payment-management.tsx b/frontend/src/components/admin/payment-management.tsx deleted file mode 100644 index cedadbcd..00000000 --- a/frontend/src/components/admin/payment-management.tsx +++ /dev/null @@ -1,271 +0,0 @@ -"use client"; - -import { List } from "@refinedev/antd"; -import { - Alert, - App, - Button, - Card, - Col, - Collapse, - DatePicker, - Form, - Input, - Row, - Select, - Space, - Statistic, - Switch, - Table, - Tag, - Typography, - type TableColumnsType, -} from "antd"; -import type { Dayjs } from "dayjs"; -import { useCallback, useEffect, useState } from "react"; - -const { Text } = Typography; - -type Order = { - orderNo: string; - userEmail: string | null; - packageName: string | null; - moneyCents: number; - credits: number; - status: string; - epayTradeNo: string | null; - createdAt: string; - paidAt: string | null; -}; - -type PaymentStats = { - totalOrders: number; - paidOrders: number; - pendingOrders: number; - failedExpiredOrders: number; - paidAmountCents: number; - grantedCredits: number; -}; - -type PaymentFilters = { status?: string; dates?: [Dayjs, Dayjs] }; -type EpaySettings = { - gatewayUrl: string; - pid: string; - notifyUrl: string; - returnUrl: string; - siteName: string; - chatEnabled: boolean; - keyConfigured: boolean; - complete: boolean; - source: "database" | "environment" | "unconfigured"; -}; -type EpaySettingsForm = Pick & { newKey?: string }; - -const initialStats: PaymentStats = { - totalOrders: 0, - paidOrders: 0, - pendingOrders: 0, - failedExpiredOrders: 0, - paidAmountCents: 0, - grantedCredits: 0, -}; -const statusLabels: Record = { pending: "待支付", paid: "已支付", failed: "失败", expired: "已过期" }; -const statusColors: Record = { pending: "gold", paid: "green", failed: "red", expired: "default" }; -const dateFormatter = new Intl.DateTimeFormat("zh-CN", { dateStyle: "medium", timeStyle: "short", timeZone: "Asia/Shanghai" }); -const pageSize = 20; - -function formatDate(value: string | null) { - return value ? dateFormatter.format(new Date(value)) : "—"; -} - -function formatMoney(cents: number) { - return `¥${(cents / 100).toFixed(2)}`; -} - -async function responsePayload(response: Response) { - const payload = await response.json().catch(() => ({})); - if (!response.ok) throw new Error(payload.error || "请求失败"); - return payload; -} - -export default function PaymentManagement() { - const { message } = App.useApp(); - const [filterForm] = Form.useForm(); - const [epayForm] = Form.useForm(); - const [orders, setOrders] = useState([]); - const [stats, setStats] = useState(initialStats); - const [paymentLoading, setPaymentLoading] = useState(true); - const [paymentError, setPaymentError] = useState(""); - const [filters, setFilters] = useState({}); - const [offset, setOffset] = useState(0); - const [total, setTotal] = useState(0); - const [epaySettings, setEpaySettings] = useState(null); - const [epayLoading, setEpayLoading] = useState(true); - const [epaySaving, setEpaySaving] = useState(false); - const [epayTesting, setEpayTesting] = useState(false); - const [epayError, setEpayError] = useState(""); - - const loadPayments = useCallback(async () => { - setPaymentLoading(true); - setPaymentError(""); - const params = new URLSearchParams({ limit: String(pageSize), offset: String(offset) }); - if (filters.status) params.set("status", filters.status); - if (filters.dates?.[0]) params.set("from", filters.dates[0].startOf("day").toISOString()); - if (filters.dates?.[1]) params.set("to", filters.dates[1].endOf("day").toISOString()); - try { - const payload = await responsePayload(await fetch(`/api/admin/payments?${params}`, { cache: "no-store" })); - setOrders(payload.orders); - setStats(payload.stats); - setTotal(payload.pagination.total); - } catch (error) { - setPaymentError(error instanceof Error ? error.message : "读取支付记录失败"); - } finally { - setPaymentLoading(false); - } - }, [filters, offset]); - - const loadEpaySettings = useCallback(async () => { - setEpayLoading(true); - setEpayError(""); - try { - const payload: EpaySettings = await responsePayload(await fetch("/api/admin/epay-settings", { cache: "no-store" })); - setEpaySettings(payload); - epayForm.setFieldsValue({ - gatewayUrl: payload.gatewayUrl, - pid: payload.pid, - notifyUrl: payload.notifyUrl, - returnUrl: payload.returnUrl, - siteName: payload.siteName, - chatEnabled: payload.chatEnabled, - newKey: "", - }); - } catch (error) { - setEpayError(error instanceof Error ? error.message : "读取易支付配置失败"); - } finally { - setEpayLoading(false); - } - }, [epayForm]); - - useEffect(() => { - const timer = window.setTimeout(() => void loadPayments(), 0); - return () => window.clearTimeout(timer); - }, [loadPayments]); - useEffect(() => { - const timer = window.setTimeout(() => void loadEpaySettings(), 0); - return () => window.clearTimeout(timer); - }, [loadEpaySettings]); - - async function testEpayAvailability() { - setEpayTesting(true); - try { - const payload = await responsePayload(await fetch("/api/admin/epay-settings/test", { method: "POST" })); - if (payload.available) message.success(`${payload.message}(${payload.status},${payload.latencyMs}ms)`); - else message.error(payload.message || "当前已保存的易支付配置暂不可用"); - } catch (error) { - message.error(error instanceof Error ? error.message : "当前已保存的易支付配置暂不可用"); - } finally { - setEpayTesting(false); - } - } - - async function saveEpaySettings(values: EpaySettingsForm) { - setEpaySaving(true); - try { - await responsePayload(await fetch("/api/admin/epay-settings", { - method: "PUT", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ ...values, newKey: values.newKey || undefined }), - })); - epayForm.setFieldValue("newKey", ""); - message.success("Z-Pay(易支付)配置已保存"); - await loadEpaySettings(); - } catch (error) { - message.error(error instanceof Error ? error.message : "保存易支付配置失败"); - } finally { - setEpaySaving(false); - } - } - - const orderColumns: TableColumnsType = [ - { title: "订单号", dataIndex: "orderNo", render: (value) => {value} }, - { title: "用户邮箱", dataIndex: "userEmail", render: (value) => value || "—" }, - { title: "套餐", dataIndex: "packageName", render: (value) => value || "—" }, - { title: "金额", dataIndex: "moneyCents", align: "right", render: formatMoney }, - { title: "点数", dataIndex: "credits", align: "right" }, - { title: "状态", dataIndex: "status", render: (value) => {statusLabels[value] || value} }, - { title: "易支付交易号", dataIndex: "epayTradeNo", render: (value) => value || "—" }, - { title: "创建时间", dataIndex: "createdAt", render: formatDate }, - { title: "支付时间", dataIndex: "paidAt", render: formatDate }, - ]; - return ( - - - - - - - - - - - - - - } - > - - 配置兼容标准 Z-Pay / 易支付协议的支付网关、商户凭据、回调地址与对话页开关。 - {epaySettings && 来源:{{ database: "数据库", environment: "环境变量", unconfigured: "未配置" }[epaySettings.source]}{epaySettings.complete ? "配置完整" : "配置不完整"}{epaySettings.keyConfigured ? "密钥已配置" : "密钥未配置"}{epaySettings.chatEnabled ? "对话支付开放" : "对话支付关闭"}} - {epayError && void loadEpaySettings()}>重试} />} - - form={epayForm} layout="vertical" onFinish={saveEpaySettings} requiredMark="optional"> - - - - - - - - - - - - - - - ), - }]} - /> - - 共 {total} 条平台订单}> - -
{ setOffset(0); setFilters(values); }}> - - - - - )} -
- {error && } - - {...tableProps} - columns={columns} - rowKey="id" - locale={{ emptyText: }} - scroll={{ x: "max-content" }} - /> -
-
- ); -} - -export function formatAdminDate(value: string | null | undefined) { - return value ? new Intl.DateTimeFormat("zh-CN", { - dateStyle: "medium", - timeStyle: "short", - }).format(new Date(value)) : "—"; -} diff --git a/frontend/src/components/admin/users-resource.tsx b/frontend/src/components/admin/users-resource.tsx deleted file mode 100644 index a4e93281..00000000 --- a/frontend/src/components/admin/users-resource.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client"; - -import { Tag, type TableColumnsType } from "antd"; - -import { formatAdminDate, ResourceTable } from "@/components/admin/resource-table"; - -type UserRecord = { - id: string; - email: string; - name: string | null; - role: string; - emailVerified: boolean; - banned: boolean; - createdAt: string; - credits: number; - birthDate: string | null; - birthTimeStatus: string | null; - birthPlace: string | null; -}; - -const columns: TableColumnsType = [ - { title: "邮箱", dataIndex: "email", sorter: true }, - { title: "姓名", dataIndex: "name", sorter: true, render: (value) => value || "—" }, - { title: "角色", dataIndex: "role", render: (value) => {value} }, - { title: "积分", dataIndex: "credits", sorter: true }, - { title: "出生日期", dataIndex: "birthDate", render: (value) => value || "—" }, - { title: "出生时间状态", dataIndex: "birthTimeStatus", render: (value) => value || "—" }, - { title: "出生地", dataIndex: "birthPlace", render: (value) => value || "—" }, - { title: "邮箱验证", dataIndex: "emailVerified", render: (value) => value ? "已验证" : "未验证" }, - { title: "状态", dataIndex: "banned", render: (value) => value ? 已禁用 : 正常 }, - { title: "注册时间", dataIndex: "createdAt", sorter: true, render: formatAdminDate }, -]; - -export default function UsersPage() { - return resource="users" title="用户资料(只读)" columns={columns} />; -} diff --git a/frontend/src/components/app-sidebar.tsx b/frontend/src/components/app-sidebar.tsx index 4de7e36e..6a4a2516 100644 --- a/frontend/src/components/app-sidebar.tsx +++ b/frontend/src/components/app-sidebar.tsx @@ -1,9 +1,11 @@ "use client"; import { Popover } from "@base-ui/react/popover"; +import Link from "next/link"; import { ChevronRight, Gift, + KeyRound, LogOut, MessageSquareText, Plus, @@ -35,6 +37,7 @@ export type SidebarAccount = { name: string; email: string; credits: number; + isAdmin: boolean; initial: string; }; @@ -211,6 +214,9 @@ export function AppSidebar({ + {account.isAdmin && onAccountMenuOpenChange(false)}> +