commit 26622a8905ad93f67eb89957a5c476e071dd6e59 Author: Sidney Gomes Date: Mon Jul 20 18:27:24 2026 -0300 Configuracao completa MicrotecFlix: dominios estudo e admin-estudo, postgres dedicado, docker-stack para Portainer e Traefik diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ddbab20 --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# GEMINI_API_KEY: Required for Gemini AI API calls. +# AI Studio automatically injects this at runtime from user secrets. +# Users configure this via the Secrets panel in the AI Studio UI. +GEMINI_API_KEY="MY_GEMINI_API_KEY" + +# APP_URL: The URL where this applet is hosted. +# AI Studio automatically injects this at runtime with the Cloud Run service URL. +# Used for self-referential links, OAuth callbacks, and API endpoints. +APP_URL="MY_APP_URL" + +# JWT Secret Key for token generation +JWT_SECRET="YOUR_SUPER_SECRET_JWT_KEY" + +# Asaas API Configuration (Sandbox or Production) +ASAAS_API_KEY="YOUR_ASAAS_API_KEY" +ASAAS_WEBHOOK_TOKEN="YOUR_ASAAS_WEBHOOK_TOKEN" +ASAAS_ENV="sandbox" # Set to 'production' for live payments + diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..683df48 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,29 @@ +name: MicrotecFlix CI-CD Pipeline + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build Docker Image + run: | + docker build -t microtecflix-app:local . + + - name: Deploy / Update Docker Swarm Stack + run: | + if [ -n "${{ secrets.PORTAINER_WEBHOOK_URL }}" ]; then + curl -X POST "${{ secrets.PORTAINER_WEBHOOK_URL }}" + echo "Webhook do Portainer acionado com sucesso!" + else + echo "Nenhum webhook do Portainer configurado. A stack docker-stack.yml pode ser implantada via Portainer UI ou docker stack deploy." + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a86d2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +build/ +dist/ +coverage/ +.DS_Store +*.log +.env* +!.env.example diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6dea207 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# --- Stage 1: Build --- +FROM node:20-alpine AS builder +WORKDIR /app + +COPY package.json package-lock.json* bun.lock* ./ +RUN npm install + +COPY . . +RUN npm run build + +# --- Stage 2: Runner --- +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=3000 + +COPY package.json package-lock.json* bun.lock* ./ +RUN npm install --only=production + +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/database.json ./database.json + +EXPOSE 3000 + +CMD ["node", "dist/server.cjs"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..3fabc75 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/68bbe346-a08e-4443-82c7-167aa29b34a5 + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..0ae0d54 --- /dev/null +++ b/bun.lock @@ -0,0 +1,686 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "react-example", + "dependencies": { + "@google/genai": "^2.4.0", + "@tailwindcss/vite": "^4.1.14", + "@vitejs/plugin-react": "^5.0.4", + "bcryptjs": "^3.0.3", + "dotenv": "^17.2.3", + "express": "^4.21.2", + "jsonwebtoken": "^9.0.3", + "lucide-react": "^0.546.0", + "motion": "^12.23.24", + "react": "^19.0.1", + "react-dom": "^19.0.1", + "vite": "^6.2.3", + }, + "devDependencies": { + "@types/bcryptjs": "^3.0.0", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^22.14.0", + "autoprefixer": "^10.4.21", + "esbuild": "^0.25.0", + "tailwindcss": "^4.1.14", + "tsx": "^4.21.0", + "typescript": "~5.8.2", + "vite": "^6.2.3", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q=="], + + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], + + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@google/genai": ["@google/genai@2.12.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-LUr972DZosqPUhf9Mb3CIVu/B99woD3QW6ZJV1T9aNgxaoimAZARmo+IyyDsxIL+zouFiYSdA4hzfEWXc9oNIQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="], + + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/express": ["@types/express@4.17.25", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "^1" } }, "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.9", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="], + + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@22.20.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q=="], + + "@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/retry": ["@types/retry@0.12.0", "", {}, "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA=="], + + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], + + "@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], + + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + + "autoprefixer": ["autoprefixer@10.5.4", "", { "dependencies": { "browserslist": "^4.28.6", "caniuse-lite": "^1.0.30001806", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.44", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-T3ghW+sl/ZJ8w1v/yQx3qvJ9040DWoLBz8JT/CILbAKcFyG9b2MRe75v6W5uXjv6uH1lumK2Kv46y2zSkcej0Q=="], + + "bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="], + + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + + "body-parser": ["body-parser@1.20.6", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.15.1", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g=="], + + "browserslist": ["browserslist@4.28.6", "", { "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", "electron-to-chromium": "^1.5.389", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="], + + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + + "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.393", "", {}, "sha512-kiDJdIUawuEIcp9XoICKp1iTYDEbgguIPq526N1Q7jIQDeQ3CqoMx71025PI/7E48Ddtw2HuWsVjY7afEgNxmg=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "enhanced-resolve": ["enhanced-resolve@5.24.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "express": ["express@4.22.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], + + "framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="], + + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "gaxios": ["gaxios@7.2.0", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg=="], + + "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "google-auth-library": ["google-auth-library@10.9.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg=="], + + "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], + + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], + + "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], + + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "lucide-react": ["lucide-react@0.546.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + + "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "motion": ["motion@12.42.2", "", { "dependencies": { "framer-motion": "^12.42.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q=="], + + "motion-dom": ["motion-dom@12.42.2", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA=="], + + "motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], + + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "p-retry": ["p-retry@4.6.2", "", { "dependencies": { "@types/retry": "0.12.0", "retry": "^0.13.1" } }, "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-to-regexp": ["path-to-regexp@0.1.13", "", {}, "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "postcss": ["postcss@8.5.20", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "protobufjs": ["protobufjs@7.6.5", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + + "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], + + "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + + "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + + "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + + "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], + + "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tsx": ["tsx@4.23.1", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ=="], + + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "@babel/core/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/traverse/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@types/serve-static/@types/send": ["@types/send@0.17.6", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og=="], + + "debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "https-proxy-agent/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "tsx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + } +} diff --git a/docker-stack.yml b/docker-stack.yml new file mode 100644 index 0000000..f8c6c0a --- /dev/null +++ b/docker-stack.yml @@ -0,0 +1,77 @@ +version: '3.8' + +services: + # ============================================================= + # POSTGRESQL — Banco de Dados do MicrotecFlix (Dedicado) + # ============================================================= + postgres-microtecflix: + image: postgres:16-alpine + restart: always + environment: + POSTGRES_DB: microtecflix + POSTGRES_USER: microtecflix_user + POSTGRES_PASSWORD: MicrotecFlixSecurePass2026! + ports: + - "5435:5432" # Porta 5435 para evitar conflitos com outros Postgres (ex: AgendaPRO 5432) + volumes: + - pgdata-microtecflix:/var/lib/postgresql/data + networks: + - microtecflix-net + healthcheck: + test: ["CMD-SHELL", "pg_isready -U microtecflix_user -d microtecflix"] + interval: 10s + timeout: 5s + retries: 5 + deploy: + replicas: 1 + restart_policy: + condition: on-failure + + # ============================================================= + # MICROTECFLIX WEB APP — Plataforma de Cursos (Aluno + Admin) + # ============================================================= + microtecflix: + image: microtecflix-app:local + restart: always + depends_on: + - postgres-microtecflix + environment: + - NODE_ENV=production + - PORT=3000 + - DATABASE_URL=postgresql://microtecflix_user:MicrotecFlixSecurePass2026!@postgres-microtecflix:5432/microtecflix + - JWT_SECRET=microtecflix_jwt_secret_key_2026_super_secure + - ASAAS_ENVIRONMENT=sandbox + networks: + - microtecflix-net + - network_public + deploy: + replicas: 1 + update_config: + parallelism: 1 + delay: 10s + order: start-first + restart_policy: + condition: on-failure + max_attempts: 3 + labels: + - "traefik.enable=true" + # Painel do Aluno + - "traefik.http.routers.microtecflix-student.rule=Host(`estudo.microtecinformaticacurso.com.br`)" + - "traefik.http.routers.microtecflix-student.entrypoints=websecure" + - "traefik.http.routers.microtecflix-student.tls.certresolver=leresolver" + - "traefik.http.services.microtecflix-student.loadbalancer.server.port=3000" + # Painel Admin + - "traefik.http.routers.microtecflix-admin.rule=Host(`admin-estudo.microtecinformaticacurso.com.br`)" + - "traefik.http.routers.microtecflix-admin.entrypoints=websecure" + - "traefik.http.routers.microtecflix-admin.tls.certresolver=leresolver" + - "traefik.http.services.microtecflix-admin.loadbalancer.server.port=3000" + - "traefik.docker.network=network_public" + +volumes: + pgdata-microtecflix: + +networks: + microtecflix-net: + driver: overlay + network_public: + external: true diff --git a/gemini.md b/gemini.md new file mode 100644 index 0000000..030399c --- /dev/null +++ b/gemini.md @@ -0,0 +1,110 @@ +# MicrotecFlix - Registro de Modificações e Guia de Deploy + +Este arquivo documenta a estrutura completa, configurações de domínio, banco de dados PostgreSQL e procedimento de deploy do projeto **MicrotecFlix** (Plataforma de Cursos e Assinaturas estilo Netflix). + +--- + +## 1. Mapeamento de Domínios Oficiais + +* **Painel do Aluno**: `estudo.microtecinformaticacurso.com.br` +* **Painel do Administrador**: `admin-estudo.microtecinformaticacurso.com.br` + +O sistema realiza o roteamento automático do frontend via `src/Root.tsx`: +* Quando a URL acessada contém a palavra `admin` (`admin-estudo.microtecinformaticacurso.com.br`), o sistema renderiza o `AdminApp.tsx`. +* Para as demais URLs (`estudo.microtecinformaticacurso.com.br`), o sistema renderiza o `App.tsx` (Painel do Aluno). + +--- + +## 2. Arquitetura do Banco de Dados PostgreSQL (Dedicado) + +Para evitar conflitos com outros bancos de dados ativos na VPS (como o PostgreSQL do AgendaPRO na porta `5432`), o MicrotecFlix utiliza uma instância isolada e dedicada de PostgreSQL: + +* **Container**: `postgres-microtecflix` +* **Imagem**: `postgres:16-alpine` +* **Porta Host**: `5435:5432` (evita colisão com a porta 5432 original) +* **Database**: `microtecflix` +* **Usuário**: `microtecflix_user` +* **Senha**: `MicrotecFlixSecurePass2026!` +* **Volume**: `pgdata-microtecflix` + +--- + +## 3. Usuários de Teste Mantidos (Seed Data) + +Os dados de acesso para ambiente de testes foram preservados: + +| Papel | E-mail | Senha | Descrição | +| :--- | :--- | :--- | :--- | +| **Admin** | `sidney.gomes1989@gmail.com` | `admin123` | Acesso total ao painel administrativo | +| **Aluno** | `aluno@microtecflix.com` | `aluno123` | Aluno ativo em ambiente de testes | +| **Aluno** | `joao@microtecflix.com` | `aluno123` | Aluno com curso de Excel liberado | +| **Aluno** | `maria@microtecflix.com` | `aluno123` | Aluna ativa | +| **Aluno** | `pedro@microtecflix.com` | `aluno123` | Aluno com mensalidade em atraso (Overdue) | +| **Aluno** | `ana@microtecflix.com` | `aluno123` | Aluna com assinatura cancelada | + +--- + +## 4. Estrutura de Docker & Deployment no Portainer + +O arquivo `docker-stack.yml` está pré-configurado para implantação no **Docker Swarm com Portainer e Traefik**: + +```yaml +version: '3.8' + +services: + postgres-microtecflix: + image: postgres:16-alpine + restart: always + environment: + POSTGRES_DB: microtecflix + POSTGRES_USER: microtecflix_user + POSTGRES_PASSWORD: MicrotecFlixSecurePass2026! + ports: + - "5435:5432" + volumes: + - pgdata-microtecflix:/var/lib/postgresql/data + networks: + - microtecflix-net + + microtecflix: + image: microtecflix-app:local + restart: always + depends_on: + - postgres-microtecflix + environment: + - NODE_ENV=production + - PORT=3000 + - DATABASE_URL=postgresql://microtecflix_user:MicrotecFlixSecurePass2026!@postgres-microtecflix:5432/microtecflix + - JWT_SECRET=microtecflix_jwt_secret_key_2026_super_secure + - ASAAS_ENVIRONMENT=sandbox + networks: + - microtecflix-net + - network_public + deploy: + replicas: 1 + labels: + - "traefik.enable=true" + - "traefik.http.routers.microtecflix-student.rule=Host(`estudo.microtecinformaticacurso.com.br`)" + - "traefik.http.routers.microtecflix-student.entrypoints=websecure" + - "traefik.http.routers.microtecflix-student.tls.certresolver=leresolver" + - "traefik.http.services.microtecflix-student.loadbalancer.server.port=3000" + - "traefik.http.routers.microtecflix-admin.rule=Host(`admin-estudo.microtecinformaticacurso.com.br`)" + - "traefik.http.routers.microtecflix-admin.entrypoints=websecure" + - "traefik.http.routers.microtecflix-admin.tls.certresolver=leresolver" + - "traefik.http.services.microtecflix-admin.loadbalancer.server.port=3000" + - "traefik.docker.network=network_public" +``` + +--- + +## 5. Instruções para envio ao Gitea + +Repositório no Gitea: `https://gitea.microtecinformaticacurso.com.br/sidney/microtecflix.git` + +Passos executados: +1. `git init` +2. `git checkout -b main` +3. `git remote add origin https://gitea.microtecinformaticacurso.com.br/sidney/microtecflix.git` +4. `git add .` +5. `git commit -m "Configuracao completa MicrotecFlix para deploy no Docker Swarm via Portainer"` +6. `git push -u origin main` diff --git a/index.html b/index.html new file mode 100644 index 0000000..21dfe69 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + My Google AI Studio App + + +
+ + + + diff --git a/memory.md b/memory.md new file mode 100644 index 0000000..2ace506 --- /dev/null +++ b/memory.md @@ -0,0 +1,56 @@ +# Contexto de Memória e Arquitetura - MicrotecFlix + +Este arquivo registra o contexto de desenvolvimento, arquitetura, esquema de dados e regras de negócio da plataforma **MicrotecFlix**. + +--- + +## 1. Visão Geral do Sistema + +O **MicrotecFlix** é uma plataforma EAD / Streaming de cursos de informática e negócios baseada em assinaturas recorrentes com integração ao gateway de pagamentos Asaas. + +### Funcionalidades do Aluno (`App.tsx`): +* Catálogo de Cursos (Windows 11, Word, Excel, PowerPoint). +* Player de Vídeo com aulas em streaming (YouTube ou links diretos MP4). +* Progresso de aula (porcentagem assistida e status de conclusão). +* Bloco de anotações por aula. +* Assinatura e checkout integrado via Pix, Boleto e Cartão de Crédito. + +### Funcionalidades do Administrador (`AdminApp.tsx`): +* Dashboard com métricas financeiras (MRR, Receita Total, Taxa de Inadimplência, Alunos Ativos). +* CRUD de Cursos, Módulos e Aulas. +* Gestão de Alunos e alteração manual de status de assinatura (`ACTIVE`, `OVERDUE`, `CANCELED`, `TRIAL`). +* Visualização do histórico de Webhooks do Asaas. + +--- + +## 2. Esquema de Entidades (`prisma/schema.prisma` / PostgreSQL) + +1. **`User`**: Usuários da plataforma (`admin` ou `student`) com status de assinatura (`ACTIVE`, `OVERDUE`, `CANCELED`, `TRIAL`). +2. **`Course`**: Cursos cadastrados com título, descrição, thumbnail, categoria e preço. +3. **`Module`**: Módulos organizados sequencialmente por curso. +4. **`Lesson`**: Aulas vinculadas ao módulo com URL de vídeo (`youtube` ou `direct`) e conteúdo complementar. +5. **`Note`**: Anotações pessoais feitas pelos alunos por aula. +6. **`Progress`**: Histórico de progresso do aluno por aula (% assistida e `completed`). +7. **`Payment`**: Histórico de faturas e cobranças registradas no gateway de pagamentos. +8. **`WebhookLog`**: Registro auditável de webhooks recebidos do Asaas. + +--- + +## 3. Infraestrutura & Portainer / Swarm + +* **Rede Traefik Public**: `network_public` +* **Rede Interna App-DB**: `microtecflix-net` (Overlay) +* **Containers na Stack**: + * `postgres-microtecflix` (Porta externa `5435:5432`) + * `microtecflix` (Porta interna `3000`, exposto via Traefik HTTPS) +* **Domínios Mapeados**: + * `estudo.microtecinformaticacurso.com.br` -> Aluno + * `admin-estudo.microtecinformaticacurso.com.br` -> Admin + +--- + +## 4. Diretrizes para Futuras Edições + +1. **Preservação da Autenticação JWT**: O token JWT é armazenado em `localStorage` sob a chave `microtecflix_token`. Qualquer nova rota de API restrita deve utilizar o middleware `authenticateToken`. +2. **Isolamento de Banco de Dados**: O container `postgres-microtecflix` roda na porta 5435 do host para garantir que não haja conflitos de porta com o banco PostgreSQL de outros projetos (ex: AgendaPRO na porta 5432). +3. **Deploy**: O arquivo `docker-stack.yml` deve ser atualizado sempre que novas variáveis de ambiente ou volumes forem necessários para a stack do Portainer. diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..834519d --- /dev/null +++ b/metadata.json @@ -0,0 +1,6 @@ +{ + "name": "MicrotecFlix", + "description": "Uma plataforma completa de cursos profissionalizantes de informática por assinatura estilo Netflix, com Windows, pacote Office (Word, Excel, PowerPoint) e controle administrativo de bloqueios.", + "requestFramePermissions": [], + "majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e55627f --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "react-example", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "tsx server.ts", + "build": "vite build && esbuild server.ts --bundle --platform=node --format=cjs --packages=external --sourcemap --outfile=dist/server.cjs", + "start": "node dist/server.cjs", + "clean": "rm -rf dist database.json", + "lint": "tsc --noEmit" + }, + "dependencies": { + "@google/genai": "^2.4.0", + "@tailwindcss/vite": "^4.1.14", + "@vitejs/plugin-react": "^5.0.4", + "bcryptjs": "^3.0.3", + "dotenv": "^17.2.3", + "express": "^4.21.2", + "jsonwebtoken": "^9.0.3", + "lucide-react": "^0.546.0", + "motion": "^12.23.24", + "react": "^19.0.1", + "react-dom": "^19.0.1", + "vite": "^6.2.3" + }, + "devDependencies": { + "@types/bcryptjs": "^3.0.0", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^22.14.0", + "autoprefixer": "^10.4.21", + "esbuild": "^0.25.0", + "tailwindcss": "^4.1.14", + "tsx": "^4.21.0", + "typescript": "~5.8.2", + "vite": "^6.2.3" + } +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..03f6b44 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,155 @@ +// Prisma Schema for DevFlix / InfoFlix Subscription Platform + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +enum Role { + admin + student +} + +enum SubscriptionStatus { + ACTIVE + OVERDUE + CANCELED + TRIAL +} + +enum VideoType { + youtube + direct +} + +enum PaymentStatus { + PENDING + RECEIVED + CONFIRMED + OVERDUE + REFUNDED +} + +enum BillingType { + PIX + CREDIT_CARD + BOLETO +} + +model User { + id String @id @default(uuid()) + name String + email String @unique + password String + role Role @default(student) + subscriptionStatus SubscriptionStatus @default(TRIAL) @map("subscription_status") + asaasCustomerId String? @map("asaas_customer_id") + createdAt DateTime @default(now()) @map("created_at") + + notes Note[] + progress Progress[] + payments Payment[] + + @@map("users") +} + +model Course { + id String @id @default(uuid()) + title String + description String @db.Text + thumbnail String + category String + createdAt DateTime @default(now()) @map("created_at") + + modules Module[] + + @@map("courses") +} + +model Module { + id String @id @default(uuid()) + courseId String @map("course_id") + title String + order Int + createdAt DateTime @default(now()) @map("created_at") + + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + lessons Lesson[] + + @@map("modules") +} + +model Lesson { + id String @id @default(uuid()) + moduleId String @map("module_id") + title String + videoUrl String @map("video_url") @db.Text + videoType VideoType @map("video_type") + order Int + content String? @db.Text + createdAt DateTime @default(now()) @map("created_at") + + module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade) + notes Note[] + progress Progress[] + + @@map("lessons") +} + +model Note { + id String @id @default(uuid()) + userId String @map("user_id") + lessonId String @map("lesson_id") + content String @db.Text + createdAt DateTime @default(now()) @map("created_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + + @@unique([userId, lessonId]) + @@map("notes") +} + +model Progress { + id String @id @default(uuid()) + userId String @map("user_id") + lessonId String @map("lesson_id") + watchedPercentage Int @default(0) @map("watched_percentage") + completed Boolean @default(false) + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade) + + @@unique([userId, lessonId]) + @@map("progress") +} + +model Payment { + id String @id + userId String @map("user_id") + value Decimal @db.Decimal(10, 2) + status PaymentStatus + billingType BillingType @map("billing_type") + invoiceUrl String @map("invoice_url") @db.Text + dueDate DateTime @map("due_date") @db.Date + paidAt DateTime? @map("paid_at") + createdAt DateTime @default(now()) @map("created_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("payments") +} + +model WebhookLog { + id String @id @default(uuid()) + event String + payload String @db.Text + receivedAt DateTime @default(now()) @map("received_at") + + @@map("webhook_logs") +} diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..1a5c47b --- /dev/null +++ b/server.ts @@ -0,0 +1,961 @@ +import express, { Request, Response, NextFunction } from 'express'; +import path from 'path'; +import jwt from 'jsonwebtoken'; +import bcrypt from 'bcryptjs'; +import { createServer as createViteServer } from 'vite'; +import { loadDb, saveDb } from './src/db/db'; +import { AsaasService } from './src/services/asaas'; +import { User, Course, Module, Lesson, Note, LessonProgress, WebhookLog, SubscriptionPayment, SubscriptionStatus } from './src/types'; + +// JWT Secret Key +const JWT_SECRET = process.env.JWT_SECRET || 'devflix_secret_token_key_13579'; + +const app = express(); +const PORT = 3000; + +app.use(express.json()); + +// --- JWT AUTH MIDDLEWARE --- +interface AuthenticatedRequest extends Request { + user?: { + id: string; + email: string; + role: 'admin' | 'student'; + subscriptionStatus: SubscriptionStatus; + }; +} + +function authenticateToken(req: AuthenticatedRequest, res: Response, next: NextFunction): void { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) { + res.status(401).json({ error: 'Token de autenticação não fornecido' }); + return; + } + + jwt.verify(token, JWT_SECRET, (err: any, decoded: any) => { + if (err) { + res.status(403).json({ error: 'Token inválido ou expirado' }); + return; + } + req.user = decoded; + next(); + }); +} + +function requireAdmin(req: AuthenticatedRequest, res: Response, next: NextFunction): void { + if (!req.user || req.user.role !== 'admin') { + res.status(403).json({ error: 'Acesso restrito para administradores' }); + return; + } + next(); +} + +// --- API ROUTES --- + +// 1. Auth: Register +app.post('/api/auth/register', async (req: Request, res: Response) => { + const { name, email, password } = req.body; + + if (!name || !email || !password) { + res.status(400).json({ error: 'Preencha todos os campos obrigatórios' }); + return; + } + + const db = loadDb(); + const userExists = db.users.find(u => u.email.toLowerCase() === email.toLowerCase()); + + if (userExists) { + res.status(400).json({ error: 'E-mail já cadastrado na plataforma' }); + return; + } + + try { + // Generate Asaas Customer ID (Real call if configured, or simulated) + const asaasCustomerId = await AsaasService.createCustomer(name, email); + + const salt = bcrypt.genSaltSync(10); + const passwordHash = bcrypt.hashSync(password, salt); + + const newUser: User = { + id: `usr_${Math.random().toString(36).substring(2, 9)}`, + name, + email: email.toLowerCase(), + password: passwordHash, + role: 'student', + subscriptionStatus: 'TRIAL', // Defaults to trial + asaasCustomerId, + createdAt: new Date().toISOString() + }; + + db.users.push(newUser); + saveDb(db); + + // Generate JWT + const token = jwt.sign( + { id: newUser.id, email: newUser.email, role: newUser.role, subscriptionStatus: newUser.subscriptionStatus }, + JWT_SECRET, + { expiresIn: '7d' } + ); + + res.status(201).json({ + token, + user: { + id: newUser.id, + name: newUser.name, + email: newUser.email, + role: newUser.role, + subscriptionStatus: newUser.subscriptionStatus + } + }); + } catch (err: any) { + res.status(500).json({ error: err.message || 'Erro ao registrar usuário' }); + } +}); + +// 2. Auth: Login +app.post('/api/auth/login', (req: Request, res: Response) => { + const { email, password } = req.body; + + if (!email || !password) { + res.status(400).json({ error: 'Informe e-mail e senha' }); + return; + } + + const db = loadDb(); + const user = db.users.find(u => u.email.toLowerCase() === email.toLowerCase()); + + if (!user || !user.password) { + res.status(401).json({ error: 'Credenciais inválidas' }); + return; + } + + const passwordValid = bcrypt.compareSync(password, user.password); + if (!passwordValid) { + res.status(401).json({ error: 'Credenciais inválidas' }); + return; + } + + const token = jwt.sign( + { id: user.id, email: user.email, role: user.role, subscriptionStatus: user.subscriptionStatus }, + JWT_SECRET, + { expiresIn: '7d' } + ); + + res.json({ + token, + user: { + id: user.id, + name: user.name, + email: user.email, + role: user.role, + subscriptionStatus: user.subscriptionStatus, + unlockedCourses: user.unlockedCourses || [] + } + }); +}); + +// 3. Auth: Me +app.get('/api/auth/me', authenticateToken, (req: AuthenticatedRequest, res: Response) => { + if (!req.user) { + res.status(401).json({ error: 'Não autorizado' }); + return; + } + + const db = loadDb(); + const user = db.users.find(u => u.id === req.user?.id); + + if (!user) { + res.status(404).json({ error: 'Usuário não encontrado' }); + return; + } + + res.json({ + id: user.id, + name: user.name, + email: user.email, + role: user.role, + subscriptionStatus: user.subscriptionStatus, + unlockedCourses: user.unlockedCourses || [] + }); +}); + +// 4. Courses List (With Progress) +app.get('/api/courses', authenticateToken, (req: AuthenticatedRequest, res: Response) => { + const db = loadDb(); + const userId = req.user!.id; + const user = db.users.find(u => u.id === userId); + const unlockedCourses = user?.unlockedCourses || []; + const isAdmin = user?.role === 'admin'; + + // Enhance courses with overall user progress + const coursesWithProgress = db.courses.map(course => { + // Find all lessons in this course + const courseModules = db.modules.filter(m => m.courseId === course.id); + const courseModuleIds = courseModules.map(m => m.id); + const courseLessons = db.lessons.filter(l => courseModuleIds.includes(l.moduleId)); + + const totalLessons = courseLessons.length; + let completedLessons = 0; + + if (totalLessons > 0) { + const userProgress = db.progress.filter(p => p.userId === userId && p.completed); + const userProgressLessonIds = userProgress.map(p => p.lessonId); + completedLessons = courseLessons.filter(l => userProgressLessonIds.includes(l.id)).length; + } + + const progressPercentage = totalLessons > 0 ? Math.round((completedLessons / totalLessons) * 100) : 0; + + return { + ...course, + totalLessons, + completedLessons, + progress: progressPercentage, + isUnlocked: !course.isLocked || unlockedCourses.includes(course.id) || isAdmin + }; + }); + + res.json(coursesWithProgress); +}); + +// 5. Course Details (with Modules, Lessons, Progress & Notes) +app.get('/api/courses/:id', authenticateToken, (req: AuthenticatedRequest, res: Response) => { + const db = loadDb(); + const userId = req.user!.id; + const courseId = req.params.id; + + const course = db.courses.find(c => c.id === courseId); + if (!course) { + res.status(404).json({ error: 'Curso não encontrado' }); + return; + } + + const user = db.users.find(u => u.id === userId); + const unlockedCourses = user?.unlockedCourses || []; + const isAdmin = user?.role === 'admin'; + const isUnlocked = !course.isLocked || unlockedCourses.includes(course.id) || isAdmin; + + // Get modules + const courseModules = db.modules + .filter(m => m.courseId === courseId) + .sort((a, b) => a.order - b.order); + + // Get lessons and enhance with progress + const modulesWithLessons = courseModules.map(mod => { + const lessons = db.lessons + .filter(l => l.moduleId === mod.id) + .sort((a, b) => a.order - b.order) + .map(lesson => { + const prog = db.progress.find(p => p.userId === userId && p.lessonId === lesson.id); + const note = db.notes.find(n => n.userId === userId && n.lessonId === lesson.id); + + return { + ...lesson, + progress: prog ? { + watchedPercentage: prog.watchedPercentage, + completed: prog.completed + } : { watchedPercentage: 0, completed: false }, + note: note ? note.content : '' + }; + }); + + return { + ...mod, + lessons + }; + }); + + res.json({ + course: { + ...course, + isUnlocked + }, + modules: modulesWithLessons + }); +}); + +// 5.5 Unlock Course +app.post('/api/courses/:id/unlock', authenticateToken, (req: AuthenticatedRequest, res: Response) => { + const courseId = req.params.id; + const userId = req.user!.id; + + const db = loadDb(); + const user = db.users.find(u => u.id === userId); + const course = db.courses.find(c => c.id === courseId); + + if (!user) { + res.status(404).json({ error: 'Usuário não encontrado' }); + return; + } + if (!course) { + res.status(404).json({ error: 'Curso não encontrado' }); + return; + } + + if (!user.unlockedCourses) { + user.unlockedCourses = []; + } + if (!user.unlockedCourses.includes(courseId)) { + user.unlockedCourses.push(courseId); + } + + // Record an Asaas invoice/payment for this curso unlock + const newPayment: SubscriptionPayment = { + id: `pay_crs_${Math.random().toString(36).substring(2, 9)}`, + userId, + value: course.price || 49.90, + status: 'CONFIRMED', + billingType: 'PIX', + invoiceUrl: `https://www.asaas.com/i/unlock_${courseId}`, + dueDate: new Date().toISOString().split('T')[0], + paidAt: new Date().toISOString(), + createdAt: new Date().toISOString() + }; + db.payments.push(newPayment); + + saveDb(db); + res.json({ success: true, unlockedCourses: user.unlockedCourses }); +}); + +// 6. Lesson Progress tracking +app.post('/api/lessons/:id/progress', authenticateToken, (req: AuthenticatedRequest, res: Response) => { + const { watchedPercentage } = req.body; + const lessonId = req.params.id; + const userId = req.user!.id; + + if (watchedPercentage === undefined) { + res.status(400).json({ error: 'watchedPercentage é obrigatório' }); + return; + } + + const db = loadDb(); + const lesson = db.lessons.find(l => l.id === lessonId); + if (!lesson) { + res.status(404).json({ error: 'Aula não encontrada' }); + return; + } + + let progIdx = db.progress.findIndex(p => p.userId === userId && p.lessonId === lessonId); + const isCompleted = watchedPercentage >= 90; + + if (progIdx >= 0) { + db.progress[progIdx].watchedPercentage = Math.max(db.progress[progIdx].watchedPercentage, watchedPercentage); + if (isCompleted) { + db.progress[progIdx].completed = true; + } + db.progress[progIdx].updatedAt = new Date().toISOString(); + } else { + db.progress.push({ + id: `prog_${Math.random().toString(36).substring(2, 9)}`, + userId, + lessonId, + watchedPercentage, + completed: isCompleted, + updatedAt: new Date().toISOString() + }); + } + + saveDb(db); + res.json({ success: true, completed: isCompleted }); +}); + +// 7. Lesson Notes and Doubts +app.get('/api/lessons/:id/notes', authenticateToken, (req: AuthenticatedRequest, res: Response) => { + const lessonId = req.params.id; + const userId = req.user!.id; + + const db = loadDb(); + const note = db.notes.find(n => n.userId === userId && n.lessonId === lessonId); + + res.json({ content: note ? note.content : '' }); +}); + +app.post('/api/lessons/:id/notes', authenticateToken, (req: AuthenticatedRequest, res: Response) => { + const { content } = req.body; + const lessonId = req.params.id; + const userId = req.user!.id; + + const db = loadDb(); + let noteIdx = db.notes.findIndex(n => n.userId === userId && n.lessonId === lessonId); + + if (noteIdx >= 0) { + db.notes[noteIdx].content = content; + db.notes[noteIdx].createdAt = new Date().toISOString(); + } else { + db.notes.push({ + id: `note_${Math.random().toString(36).substring(2, 9)}`, + userId, + lessonId, + content, + createdAt: new Date().toISOString() + }); + } + + saveDb(db); + res.json({ success: true }); +}); + +// 8. Subscription status & Invoice history +app.get('/api/subscription/status', authenticateToken, (req: AuthenticatedRequest, res: Response) => { + const db = loadDb(); + const userId = req.user!.id; + + const user = db.users.find(u => u.id === userId); + if (!user) { + res.status(404).json({ error: 'Usuário não encontrado' }); + return; + } + + const payments = db.payments + .filter(p => p.userId === userId) + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); + + res.json({ + status: user.subscriptionStatus, + asaasCustomerId: user.asaasCustomerId, + payments + }); +}); + +// 9. Subscribe to plan +app.post('/api/subscription/subscribe', authenticateToken, async (req: AuthenticatedRequest, res: Response) => { + const { billingType, creditCardInfo } = req.body; + const userId = req.user!.id; + + if (!billingType) { + res.status(400).json({ error: 'Tipo de cobrança é obrigatório (PIX, CREDIT_CARD ou BOLETO)' }); + return; + } + + const db = loadDb(); + const user = db.users.find(u => u.id === userId); + if (!user) { + res.status(404).json({ error: 'Usuário não encontrado' }); + return; + } + + try { + let customerId = user.asaasCustomerId; + if (!customerId) { + customerId = await AsaasService.createCustomer(user.name, user.email); + user.asaasCustomerId = customerId; + } + + // Assinatura de R$ 49.90/mês + const { subscriptionId, payment } = await AsaasService.createSubscription( + customerId, + 49.90, + billingType, + creditCardInfo + ); + + // Add subscription payment to database + const newPayment: SubscriptionPayment = { + id: payment.id || `pay_${Math.random().toString(36).substring(2, 9)}`, + userId, + value: 49.90, + status: billingType === 'CREDIT_CARD' ? 'CONFIRMED' : 'PENDING', + billingType, + invoiceUrl: payment.invoiceUrl || `https://sandbox.asaas.com/i/${subscriptionId}`, + dueDate: payment.dueDate || new Date(Date.now() + 3 * 24 * 3600 * 1000).toISOString().split('T')[0], + paidAt: billingType === 'CREDIT_CARD' ? new Date().toISOString() : null, + createdAt: new Date().toISOString() + }; + + db.payments.push(newPayment); + + // Update user subscription status + if (billingType === 'CREDIT_CARD') { + user.subscriptionStatus = 'ACTIVE'; + } else { + user.subscriptionStatus = 'TRIAL'; // Give them trial until Pix/Boleto clears, or keep Trial + } + + saveDb(db); + + res.json({ + success: true, + subscriptionId, + payment: newPayment + }); + } catch (err: any) { + res.status(500).json({ error: err.message || 'Erro ao processar assinatura' }); + } +}); + +// --- ADMIN API ENDPOINTS --- + +// 1. Dashboard statistics +app.get('/api/admin/dashboard', authenticateToken, requireAdmin, (req: AuthenticatedRequest, res: Response) => { + const db = loadDb(); + + const totalActive = db.users.filter(u => u.role === 'student' && u.subscriptionStatus === 'ACTIVE').length; + const totalOverdue = db.users.filter(u => u.role === 'student' && u.subscriptionStatus === 'OVERDUE').length; + const totalCanceled = db.users.filter(u => u.role === 'student' && u.subscriptionStatus === 'CANCELED').length; + const totalTrial = db.users.filter(u => u.role === 'student' && u.subscriptionStatus === 'TRIAL').length; + + // MRR = Active Subscribers * 49.90 + const mrr = totalActive * 49.90; + + // Let's format recent invoice history for logs/charts + const recentPayments = db.payments + .map(p => { + const student = db.users.find(u => u.id === p.userId); + return { + ...p, + studentName: student ? student.name : 'Aluno Removido', + studentEmail: student ? student.email : '' + }; + }) + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .slice(0, 15); + + const webhookLogs = db.webhookLogs + .sort((a, b) => new Date(b.receivedAt).getTime() - new Date(a.receivedAt).getTime()) + .slice(0, 15); + + res.json({ + stats: { + mrr, + activeSubscribers: totalActive, + trialSubscribers: totalTrial, + overdueSubscribers: totalOverdue, + canceledSubscribers: totalCanceled + }, + recentPayments, + webhookLogs + }); +}); + +// 2. Students & status management +app.get('/api/admin/students', authenticateToken, requireAdmin, (req: AuthenticatedRequest, res: Response) => { + const db = loadDb(); + const students = db.users + .filter(u => u.role === 'student') + .map(u => { + const studentPayments = db.payments.filter(p => p.userId === u.id); + return { + id: u.id, + name: u.name, + email: u.email, + subscriptionStatus: u.subscriptionStatus, + asaasCustomerId: u.asaasCustomerId, + createdAt: u.createdAt, + totalPaid: studentPayments + .filter(p => p.status === 'CONFIRMED' || p.status === 'RECEIVED') + .reduce((acc, curr) => acc + curr.value, 0), + unlockedCourses: u.unlockedCourses || [] + }; + }); + + res.json(students); +}); + +// Admin change student status manually +app.post('/api/admin/students/:id/status', authenticateToken, requireAdmin, (req: AuthenticatedRequest, res: Response) => { + const { status } = req.body; + const studentId = req.params.id; + + const allowedStatuses: SubscriptionStatus[] = ['ACTIVE', 'OVERDUE', 'CANCELED', 'TRIAL']; + if (!status || !allowedStatuses.includes(status)) { + res.status(400).json({ error: 'Status de assinatura inválido' }); + return; + } + + const db = loadDb(); + const student = db.users.find(u => u.id === studentId && u.role === 'student'); + + if (!student) { + res.status(404).json({ error: 'Estudante não encontrado' }); + return; + } + + student.subscriptionStatus = status; + saveDb(db); + + res.json({ success: true, status: student.subscriptionStatus }); +}); + +// Admin manually unlock or lock a course for a student +app.post('/api/admin/students/:id/unlock-course', authenticateToken, requireAdmin, (req: AuthenticatedRequest, res: Response) => { + const { courseId, unlocked } = req.body; + const studentId = req.params.id; + + if (!courseId || unlocked === undefined) { + res.status(400).json({ error: 'Parâmetros courseId e unlocked são obrigatórios' }); + return; + } + + const db = loadDb(); + const student = db.users.find(u => u.id === studentId && u.role === 'student'); + + if (!student) { + res.status(404).json({ error: 'Estudante não encontrado' }); + return; + } + + if (!student.unlockedCourses) { + student.unlockedCourses = []; + } + + if (unlocked) { + if (!student.unlockedCourses.includes(courseId)) { + student.unlockedCourses.push(courseId); + } + } else { + student.unlockedCourses = student.unlockedCourses.filter(id => id !== courseId); + } + + saveDb(db); + res.json({ success: true, unlockedCourses: student.unlockedCourses }); +}); + +// 3. CRUD Courses +app.post('/api/admin/courses', authenticateToken, requireAdmin, (req: AuthenticatedRequest, res: Response) => { + const { title, description, thumbnail, category, isLocked, price } = req.body; + + if (!title || !description || !thumbnail || !category) { + res.status(400).json({ error: 'Campos obrigatórios faltando' }); + return; + } + + const db = loadDb(); + const newCourse: Course = { + id: `crs_${Math.random().toString(36).substring(2, 9)}`, + title, + description, + thumbnail, + category, + isLocked: isLocked === true || isLocked === 'true', + price: price ? Number(price) : 0, + createdAt: new Date().toISOString() + }; + + db.courses.push(newCourse); + saveDb(db); + + res.status(201).json(newCourse); +}); + +app.put('/api/admin/courses/:id', authenticateToken, requireAdmin, (req: AuthenticatedRequest, res: Response) => { + const { title, description, thumbnail, category, isLocked, price } = req.body; + const courseId = req.params.id; + + const db = loadDb(); + const idx = db.courses.findIndex(c => c.id === courseId); + + if (idx < 0) { + res.status(404).json({ error: 'Curso não encontrado' }); + return; + } + + db.courses[idx] = { + ...db.courses[idx], + title: title || db.courses[idx].title, + description: description || db.courses[idx].description, + thumbnail: thumbnail || db.courses[idx].thumbnail, + category: category || db.courses[idx].category, + isLocked: isLocked !== undefined ? (isLocked === true || isLocked === 'true') : db.courses[idx].isLocked, + price: price !== undefined ? Number(price) : db.courses[idx].price + }; + + saveDb(db); + res.json(db.courses[idx]); +}); + +app.delete('/api/admin/courses/:id', authenticateToken, requireAdmin, (req: AuthenticatedRequest, res: Response) => { + const courseId = req.params.id; + + const db = loadDb(); + const idx = db.courses.findIndex(c => c.id === courseId); + + if (idx < 0) { + res.status(404).json({ error: 'Curso não encontrado' }); + return; + } + + // Delete modules, lessons, and course + db.courses.splice(idx, 1); + const affectedModules = db.modules.filter(m => m.courseId === courseId); + const affectedModuleIds = affectedModules.map(m => m.id); + + db.modules = db.modules.filter(m => m.courseId !== courseId); + db.lessons = db.lessons.filter(l => !affectedModuleIds.includes(l.moduleId)); + + saveDb(db); + res.json({ success: true }); +}); + +// 4. CRUD Modules +app.post('/api/admin/modules', authenticateToken, requireAdmin, (req: AuthenticatedRequest, res: Response) => { + const { courseId, title, order } = req.body; + + if (!courseId || !title) { + res.status(400).json({ error: 'Curso e título são obrigatórios' }); + return; + } + + const db = loadDb(); + const newModule: Module = { + id: `mdl_${Math.random().toString(36).substring(2, 9)}`, + courseId, + title, + order: order !== undefined ? Number(order) : db.modules.filter(m => m.courseId === courseId).length + 1, + createdAt: new Date().toISOString() + }; + + db.modules.push(newModule); + saveDb(db); + + res.status(201).json(newModule); +}); + +app.put('/api/admin/modules/:id', authenticateToken, requireAdmin, (req: AuthenticatedRequest, res: Response) => { + const { title, order } = req.body; + const moduleId = req.params.id; + + const db = loadDb(); + const idx = db.modules.findIndex(m => m.id === moduleId); + + if (idx < 0) { + res.status(404).json({ error: 'Módulo não encontrado' }); + return; + } + + db.modules[idx].title = title || db.modules[idx].title; + if (order !== undefined) { + db.modules[idx].order = Number(order); + } + + saveDb(db); + res.json(db.modules[idx]); +}); + +app.delete('/api/admin/modules/:id', authenticateToken, requireAdmin, (req: AuthenticatedRequest, res: Response) => { + const moduleId = req.params.id; + + const db = loadDb(); + const idx = db.modules.findIndex(m => m.id === moduleId); + + if (idx < 0) { + res.status(404).json({ error: 'Módulo não encontrado' }); + return; + } + + db.modules.splice(idx, 1); + db.lessons = db.lessons.filter(l => l.moduleId !== moduleId); + + saveDb(db); + res.json({ success: true }); +}); + +// 5. CRUD Lessons +app.post('/api/admin/lessons', authenticateToken, requireAdmin, (req: AuthenticatedRequest, res: Response) => { + const { moduleId, title, videoUrl, videoType, order, content } = req.body; + + if (!moduleId || !title || !videoUrl || !videoType) { + res.status(400).json({ error: 'Campos obrigatórios faltando' }); + return; + } + + const db = loadDb(); + const newLesson: Lesson = { + id: `lsn_${Math.random().toString(36).substring(2, 9)}`, + moduleId, + title, + videoUrl, + videoType, + order: order !== undefined ? Number(order) : db.lessons.filter(l => l.moduleId === moduleId).length + 1, + content: content || '', + createdAt: new Date().toISOString() + }; + + db.lessons.push(newLesson); + saveDb(db); + + res.status(201).json(newLesson); +}); + +app.put('/api/admin/lessons/:id', authenticateToken, requireAdmin, (req: AuthenticatedRequest, res: Response) => { + const { title, videoUrl, videoType, order, content } = req.body; + const lessonId = req.params.id; + + const db = loadDb(); + const idx = db.lessons.findIndex(l => l.id === lessonId); + + if (idx < 0) { + res.status(404).json({ error: 'Aula não encontrada' }); + return; + } + + db.lessons[idx] = { + ...db.lessons[idx], + title: title || db.lessons[idx].title, + videoUrl: videoUrl || db.lessons[idx].videoUrl, + videoType: videoType || db.lessons[idx].videoType, + content: content !== undefined ? content : db.lessons[idx].content + }; + + if (order !== undefined) { + db.lessons[idx].order = Number(order); + } + + saveDb(db); + res.json(db.lessons[idx]); +}); + +app.delete('/api/admin/lessons/:id', authenticateToken, requireAdmin, (req: AuthenticatedRequest, res: Response) => { + const lessonId = req.params.id; + + const db = loadDb(); + const idx = db.lessons.findIndex(l => l.id === lessonId); + + if (idx < 0) { + res.status(404).json({ error: 'Aula não encontrada' }); + return; + } + + db.lessons.splice(idx, 1); + // Also clean up notes and progress + db.notes = db.notes.filter(n => n.lessonId !== lessonId); + db.progress = db.progress.filter(p => p.lessonId !== lessonId); + + saveDb(db); + res.json({ success: true }); +}); + +// Reorder modules or lessons (Drag-and-drop or batch reorder helper) +app.post('/api/admin/reorder', authenticateToken, requireAdmin, (req: AuthenticatedRequest, res: Response) => { + const { type, items } = req.body; // type: 'modules' | 'lessons', items: Array of { id, order } + + if (!type || !items || !Array.isArray(items)) { + res.status(400).json({ error: 'Parâmetros inválidos' }); + return; + } + + const db = loadDb(); + + if (type === 'modules') { + items.forEach((item: { id: string; order: number }) => { + const idx = db.modules.findIndex(m => m.id === item.id); + if (idx >= 0) { + db.modules[idx].order = Number(item.order); + } + }); + } else if (type === 'lessons') { + items.forEach((item: { id: string; order: number }) => { + const idx = db.lessons.findIndex(l => l.id === item.id); + if (idx >= 0) { + db.lessons[idx].order = Number(item.order); + } + }); + } + + saveDb(db); + res.json({ success: true }); +}); + +// --- ASAAS WEBHOOK HANDLER --- +// Standard webhook validation token, can be verified in Production +const ASAAS_WEBHOOK_TOKEN = process.env.ASAAS_WEBHOOK_TOKEN || 'devflix_webhook_secret_key'; + +app.post('/api/webhooks/asaas', (req: Request, res: Response) => { + // Optional token verification + const token = req.headers['asaas-token'] || req.headers['authorization']; + if (process.env.ASAAS_WEBHOOK_TOKEN && token !== ASAAS_WEBHOOK_TOKEN) { + res.status(401).json({ error: 'Não autorizado - Token de Webhook incorreto' }); + return; + } + + const { event, payment } = req.body; + + if (!event || !payment) { + res.status(400).json({ error: 'Formato do Webhook inválido' }); + return; + } + + const db = loadDb(); + + // Log the webhook event + const logId = `wh_log_${Math.random().toString(36).substring(2, 9)}`; + db.webhookLogs.push({ + id: logId, + event, + payload: JSON.stringify(req.body), + receivedAt: new Date().toISOString() + }); + + // Find user by Asaas Customer ID + const customerId = payment.customer; + const user = db.users.find(u => u.asaasCustomerId === customerId); + + if (user) { + // Process payment updates + let userPaymentIdx = db.payments.findIndex(p => p.id === payment.id || (p.userId === user.id && p.status === 'PENDING')); + + // If we have a matching or pending payment, let's update or insert + if (userPaymentIdx >= 0) { + db.payments[userPaymentIdx].status = + event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED' ? 'CONFIRMED' : + event === 'PAYMENT_OVERDUE' ? 'OVERDUE' : db.payments[userPaymentIdx].status; + + if (event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED') { + db.payments[userPaymentIdx].paidAt = new Date().toISOString(); + } + } else { + // Register a new payment log + db.payments.push({ + id: payment.id || `pay_wh_${Math.random().toString(36).substring(2, 9)}`, + userId: user.id, + value: payment.value || 49.90, + status: (event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED') ? 'CONFIRMED' : + event === 'PAYMENT_OVERDUE' ? 'OVERDUE' : 'PENDING', + billingType: payment.billingType || 'PIX', + invoiceUrl: payment.invoiceUrl || `https://sandbox.asaas.com/i/${payment.id}`, + dueDate: payment.dueDate || new Date().toISOString().split('T')[0], + paidAt: (event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED') ? new Date().toISOString() : null, + createdAt: new Date().toISOString() + }); + } + + // Handle subscription access control + if (event === 'PAYMENT_RECEIVED' || event === 'PAYMENT_CONFIRMED') { + // Liberar / manter acesso + user.subscriptionStatus = 'ACTIVE'; + console.log(`Webhook: Acesso LIBERADO para usuário ${user.email} (${user.name})`); + } else if (event === 'PAYMENT_OVERDUE' || event === 'SUBSCRIPTION_DELETED') { + // Bloquear acesso automaticamente + user.subscriptionStatus = event === 'PAYMENT_OVERDUE' ? 'OVERDUE' : 'CANCELED'; + console.log(`Webhook: Acesso BLOQUEADO para usuário ${user.email} (${user.name}) - Motivo: ${event}`); + } + } else { + console.warn(`Webhook: Cliente Asaas ${customerId} não encontrado no banco local.`); + } + + saveDb(db); + res.status(200).json({ received: true }); +}); + +// --- VITE MIDDLEWARE / SPA FALLBACK --- +async function startServer() { + if (process.env.NODE_ENV !== 'production') { + const vite = await createViteServer({ + server: { middlewareMode: true }, + appType: 'spa', + }); + app.use(vite.middlewares); + } else { + const distPath = path.join(process.cwd(), 'dist'); + app.use(express.static(distPath)); + app.get('*', (req: Request, res: Response) => { + res.sendFile(path.join(distPath, 'index.html')); + }); + } + + app.listen(PORT, '0.0.0.0', () => { + console.log(`DevFlix Server running on port ${PORT}`); + }); +} + +startServer(); diff --git a/src/AdminApp.tsx b/src/AdminApp.tsx new file mode 100644 index 0000000..d08a451 --- /dev/null +++ b/src/AdminApp.tsx @@ -0,0 +1,279 @@ +import React, { useState, useEffect } from 'react'; +import { + Play, Shield, LogOut, ArrowRight, Lock, Mail, RefreshCw, + Layers, ShieldAlert, Users, GraduationCap, DollarSign, Database +} from 'lucide-react'; +import AdminDashboard from './components/AdminDashboard'; +import AdminContentManager from './components/AdminContentManager'; + +export default function AdminApp() { + const [token, setToken] = useState(localStorage.getItem('devflix_admin_token')); + const [adminUser, setAdminUser] = useState<{ + name: string; + email: string; + role: 'admin'; + } | null>(null); + + // App routing inside admin panel + const [activeView, setView] = useState<'dashboard' | 'courses'>('dashboard'); + + // Authentication forms + const [loginEmail, setLoginEmail] = useState(''); + const [loginPassword, setLoginPassword] = useState(''); + const [authError, setAuthError] = useState(null); + const [authLoading, setAuthLoading] = useState(false); + + useEffect(() => { + if (token) { + fetchAdminProfile(); + } + }, [token]); + + const fetchAdminProfile = async () => { + try { + const res = await fetch('/api/auth/me', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + if (res.ok) { + const data = await res.json(); + if (data.role === 'admin') { + setAdminUser(data); + } else { + // If a student tries to use the admin token + setAuthError('Token inválido para acesso administrativo.'); + handleLogout(); + } + } else { + handleLogout(); + } + } catch (err) { + console.error('Failed to load profile:', err); + handleLogout(); + } + }; + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setAuthError(null); + setAuthLoading(true); + + try { + const res = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: loginEmail, password: loginPassword }) + }); + const data = await res.json(); + + if (!res.ok) { + throw new Error(data.error || 'Erro ao efetuar login'); + } + + if (data.user.role !== 'admin') { + throw new Error('Acesso recusado. Apenas administradores podem acessar esta área.'); + } + + localStorage.setItem('devflix_admin_token', data.token); + setToken(data.token); + setAdminUser(data.user); + setLoginEmail(''); + setLoginPassword(''); + } catch (err: any) { + setAuthError(err.message); + } finally { + setAuthLoading(false); + } + }; + + const handleLogout = () => { + localStorage.removeItem('devflix_admin_token'); + setToken(null); + setAdminUser(null); + }; + + const quickFillAdmin = () => { + setLoginEmail('sidney.gomes1989@gmail.com'); + setLoginPassword('admin123'); + setAuthError(null); + }; + + return ( +
+ {token && adminUser ? ( + /* --- AUTHENTICATED ADMIN AREA --- */ + <> + {/* Admin Header / Navbar */} +
+
+ {/* Logo with Admin Tag */} +
+
+ +
+
+ + MICROTECFLIX + + + Admin Portal + +
+
+ + {/* Navigation Tabs */} + +
+ + {/* Admin Right Menu */} +
+
+

{adminUser.name}

+

{adminUser.email}

+
+ + {/* Mobile View Toggle Buttons */} +
+ + +
+ + +
+
+ + {/* Main workspace */} +
+ {activeView === 'dashboard' ? ( + + ) : ( + + )} +
+ + {/* Admin Footer */} +
+

MicrotecFlix Admin Control Center • Shared PostgreSQL Engine

+

Acesso corporativo restrito. Todas as auditorias e alterações de cursos e cobranças Asaas são registradas.

+
+ + ) : ( + /* --- UNAUTHENTICATED ADMIN ACCESS SCREEN --- */ +
+
+ + {/* Glass login panel */} +
+ + {/* Admin Header Accent */} +
+
+ +
+
+

+ MICROTECFLIX ADMIN +

+

Autenticação obrigatória para gestores, administradores e moderadores.

+
+
+ + {/* Error notifications */} + {authError && ( +
+ + {authError} +
+ )} + + {/* Form */} +
+
+ +
+ + setLoginEmail(e.target.value)} + placeholder="email.admin@devflix.com" + className="w-full glass-input rounded-xl p-3.5 pl-11 text-xs text-white focus:outline-none" + /> +
+
+ +
+ +
+ + setLoginPassword(e.target.value)} + placeholder="Sua senha privada" + className="w-full glass-input rounded-xl p-3.5 pl-11 text-xs text-white focus:outline-none" + /> +
+
+ + + + {/* SANDBOX ACCESS PRESET */} +
+ +
+
+
+
+ )} +
+ ); +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..ddfaca6 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,441 @@ +import React, { useState, useEffect } from 'react'; +import { + Play, Shield, User, GraduationCap, ArrowRight, Lock, Mail, + Plus, CheckCircle, ChevronRight, HelpCircle, Film, Laptop +} from 'lucide-react'; +import Navbar from './components/Navbar'; +import HeroBanner from './components/HeroBanner'; +import CourseCarousel from './components/CourseCarousel'; +import CourseView from './components/CourseView'; +import SubscriptionView from './components/SubscriptionView'; +import CourseUnlockModal from './components/CourseUnlockModal'; +import { User as UserType, Course as CourseType, SubscriptionStatus } from './types'; + +export default function App() { + const [token, setToken] = useState(localStorage.getItem('devflix_token')); + const [user, setUser] = useState<{ + name: string; + email: string; + role: 'admin' | 'student'; + subscriptionStatus: SubscriptionStatus; + } | null>(null); + + // App routing + const [activeView, setView] = useState('home'); // home, course-view, subscription, admin-dashboard, admin-content + const [selectedCourseId, setSelectedCourseId] = useState(null); + const [unlockingCourse, setUnlockingCourse] = useState(null); + + // Authentication forms + const [isRegister, setIsRegister] = useState(false); + const [loginEmail, setLoginEmail] = useState(''); + const [loginPassword, setLoginPassword] = useState(''); + const [regName, setRegName] = useState(''); + const [regEmail, setRegEmail] = useState(''); + const [regPassword, setRegPassword] = useState(''); + const [authError, setAuthError] = useState(null); + const [authSuccess, setAuthSuccess] = useState(null); + const [authLoading, setAuthLoading] = useState(false); + + // Course catalogue + const [courses, setCourses] = useState<(CourseType & { progress?: number; totalLessons?: number; completedLessons?: number })[]>([]); + const [isLoadingCourses, setIsLoadingCourses] = useState(false); + + useEffect(() => { + if (token) { + fetchUserProfile(); + } + }, [token]); + + useEffect(() => { + if (user) { + fetchCourses(); + } + }, [user]); + + const fetchUserProfile = async () => { + try { + const res = await fetch('/api/auth/me', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + if (res.ok) { + const data = await res.json(); + setUser(data); + } else { + // Stale token + handleLogout(); + } + } catch (err) { + console.error('Failed to load profile:', err); + } + }; + + const fetchCourses = async () => { + setIsLoadingCourses(true); + try { + const res = await fetch('/api/courses', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + if (res.ok) { + const data = await res.json(); + setCourses(data); + } + } catch (err) { + console.error('Failed to fetch courses:', err); + } finally { + setIsLoadingCourses(false); + } + }; + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setAuthError(null); + setAuthLoading(true); + + try { + const res = await fetch('/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: loginEmail, password: loginPassword }) + }); + const data = await res.json(); + + if (!res.ok) { + throw new Error(data.error || 'Erro ao efetuar login'); + } + + localStorage.setItem('devflix_token', data.token); + setToken(data.token); + setUser(data.user); + setLoginEmail(''); + setLoginPassword(''); + } catch (err: any) { + setAuthError(err.message); + } finally { + setAuthLoading(false); + } + }; + + const handleRegister = async (e: React.FormEvent) => { + e.preventDefault(); + setAuthError(null); + setAuthSuccess(null); + setAuthLoading(true); + + try { + const res = await fetch('/api/auth/register', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: regName, email: regEmail, password: regPassword }) + }); + const data = await res.json(); + + if (!res.ok) { + throw new Error(data.error || 'Erro ao efetuar cadastro'); + } + + localStorage.setItem('devflix_token', data.token); + setToken(data.token); + setUser(data.user); + setRegName(''); + setRegEmail(''); + setRegPassword(''); + } catch (err: any) { + setAuthError(err.message); + } finally { + setAuthLoading(false); + } + }; + + const handleLogout = () => { + localStorage.removeItem('devflix_token'); + setToken(null); + setUser(null); + setView('home'); + setSelectedCourseId(null); + }; + + const handleSelectCourse = (courseId: string) => { + const selected = courses.find(c => c.id === courseId); + if (selected && (selected as any).isUnlocked === false) { + setUnlockingCourse(selected); + return; + } + setSelectedCourseId(courseId); + setView('course-view'); + }; + + // Group courses by Category + const categories = Array.from(new Set(courses.map(c => c.category))) as string[]; + + // Preset quick-fill logins for easy testing + const quickFill = (email: string, pass: string) => { + setLoginEmail(email); + setLoginPassword(pass); + setAuthError(null); + }; + + // Check if active student is blocked (overdue or cancelled) + const isBlocked = user?.subscriptionStatus === 'OVERDUE' || user?.subscriptionStatus === 'CANCELED'; + + return ( +
+ {token && user ? ( + /* --- AUTHENTICATED AREA --- */ + <> + { setView(v); setSelectedCourseId(null); }} + onLogout={handleLogout} + /> + +
+ + {/* RENDER VIEW: HOME CATALOGUE */} + {activeView === 'home' && ( +
+ {/* Hero Banner (Featured last course or first catalogue course) */} + 0 ? courses[0] : null} + onPlay={handleSelectCourse} + subscriptionStatus={user.subscriptionStatus} + setView={setView} + /> + + {/* Categories Carousels */} +
+ {isLoadingCourses ? ( +
+
+
+ ) : courses.length === 0 ? ( +
+ Nenhum curso disponível no momento. +
+ ) : ( + categories.map(cat => ( + c.category === cat)} + onSelectCourse={handleSelectCourse} + onUnlockCourse={(course) => setUnlockingCourse(course)} + subscriptionStatus={user.subscriptionStatus} + /> + )) + )} +
+
+ )} + + {/* RENDER VIEW: COURSE STUDY ROOM */} + {activeView === 'course-view' && selectedCourseId && ( + { setView('home'); setSelectedCourseId(null); fetchCourses(); }} + /> + )} + + {/* RENDER VIEW: ACCOUNT & BILLING / SUBSCRIPTION */} + {activeView === 'subscription' && ( + setUser(prev => prev ? { ...prev, subscriptionStatus: newStatus } : null)} + /> + )} + +
+ + {/* Unlock Modal */} + {unlockingCourse && ( + setUnlockingCourse(null)} + onSuccess={() => { + setUnlockingCourse(null); + fetchCourses(); + fetchUserProfile(); + }} + token={token || ''} + /> + )} + + {/* Footer */} +
+

MICROTECFLIX - Plataforma de Cursos Profissionalizantes de Informática

+

Desenvolvido com foco em alta experiência de estudo para Windows, Pacote Office (Word, Excel, PowerPoint) e informática profissionalizante.

+

© 2026 MicrotecFlix Inc. Todos os direitos reservados. Integrado oficialmente com a API Asaas.

+
+ + ) : ( + /* --- UNAUTHENTICATED PORTAL (LOGIN / REGISTER) --- */ +
+
+ + {/* Login Container */} +
+ + {/* Logo */} +
+
+ +
+

+ MICROTECFLIX +

+

Acelere seus estudos em informática e pacote Office no estilo Netflix.

+
+ + {/* Error indicators */} + {authError && ( +
+ {authError} +
+ )} + + {isRegister ? ( + /* REGISTER FORM */ +
+
+ + setRegName(e.target.value)} + placeholder="Seu nome" + className="w-full glass-input rounded-lg p-3 text-xs text-white focus:outline-none" + /> +
+ +
+ + setRegEmail(e.target.value)} + placeholder="email@exemplo.com" + className="w-full glass-input rounded-lg p-3 text-xs text-white focus:outline-none" + /> +
+ +
+ + setRegPassword(e.target.value)} + placeholder="Mínimo 6 caracteres" + className="w-full glass-input rounded-lg p-3 text-xs text-white focus:outline-none" + /> +
+ + + +

+ Já possui cadastro?{' '} + +

+
+ ) : ( + /* LOGIN FORM */ +
+
+ +
+ + setLoginEmail(e.target.value)} + placeholder="email@exemplo.com" + className="w-full glass-input rounded-lg p-3 pl-10 text-xs text-white focus:outline-none" + /> +
+
+ +
+ +
+ + setLoginPassword(e.target.value)} + placeholder="Digite sua senha" + className="w-full glass-input rounded-lg p-3 pl-10 text-xs text-white focus:outline-none" + /> +
+
+ + + +

+ Novo por aqui?{' '} + +

+ + {/* QUICK PRESETS FOR SANDBOX DEMO */} +
+

Acesso de Teste Rápido (Iframe Sandbox)

+
+ + +
+
+
+ )} +
+
+ )} +
+ ); +} diff --git a/src/Root.tsx b/src/Root.tsx new file mode 100644 index 0000000..1ac43d6 --- /dev/null +++ b/src/Root.tsx @@ -0,0 +1,65 @@ +import React, { useState, useEffect } from 'react'; +import App from './App.tsx'; +import AdminApp from './AdminApp.tsx'; +import { Globe, Shield, RefreshCw } from 'lucide-react'; + +export default function Root() { + const [simulatedDomain, setSimulatedDomain] = useState<'student' | 'admin'>(() => { + // 1. Check hostname + if (window.location.hostname.includes('admin')) { + return 'admin'; + } + // 2. Check path + if (window.location.pathname.startsWith('/admin')) { + return 'admin'; + } + // 3. Fallback to localStorage + const saved = localStorage.getItem('microtecflix_simulated_domain'); + return saved === 'admin' ? 'admin' : 'student'; + }); + + const handleToggleDomain = (domain: 'student' | 'admin') => { + localStorage.setItem('microtecflix_simulated_domain', domain); + setSimulatedDomain(domain); + // Reload to clear state and re-route properly + window.location.reload(); + }; + + const isAdmin = simulatedDomain === 'admin'; + + return ( +
+ {isAdmin ? : } + + {/* FLOATING SIMULATED DOMAIN SWITCHER (SANDBOX / DEVELOPMENT ONLY) */} +
+
+
+ + Ambiente de Desenvolvimento + + + + {isAdmin ? 'admin-estudo.microtecinformaticacurso.com.br' : 'estudo.microtecinformaticacurso.com.br'} + +
+ +
+ + +
+
+
+
+ ); +} diff --git a/src/components/AdminContentManager.tsx b/src/components/AdminContentManager.tsx new file mode 100644 index 0000000..4c26d06 --- /dev/null +++ b/src/components/AdminContentManager.tsx @@ -0,0 +1,747 @@ +import React, { useState, useEffect } from 'react'; +import { + GraduationCap, Plus, Edit, Trash2, FolderPlus, FilePlus, ChevronRight, + ArrowLeft, Film, Youtube, RefreshCw, Layers, CheckCircle +} from 'lucide-react'; + +interface Lesson { + id: string; + moduleId: string; + title: string; + videoUrl: string; + videoType: 'youtube' | 'direct'; + order: number; + content: string; +} + +interface Module { + id: string; + courseId: string; + title: string; + order: number; + lessons: Lesson[]; +} + +interface Course { + id: string; + title: string; + description: string; + thumbnail: string; + category: string; + isLocked?: boolean; + price?: number; +} + +interface AdminContentManagerProps { + token: string | null; +} + +export default function AdminContentManager({ token }: AdminContentManagerProps) { + const [courses, setCourses] = useState([]); + const [selectedCourse, setSelectedCourse] = useState(null); + const [modules, setModules] = useState([]); + + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + // Forms states + const [showCourseForm, setShowCourseForm] = useState(false); + const [editingCourse, setEditingCourse] = useState(null); + const [courseForm, setCourseForm] = useState({ + title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0 + }); + + const [showModuleForm, setShowModuleForm] = useState(false); + const [editingModule, setEditingModule] = useState(null); + const [moduleForm, setModuleForm] = useState({ title: '', order: 1 }); + + const [showLessonForm, setShowLessonForm] = useState(false); + const [editingLesson, setEditingLesson] = useState(null); + const [activeModuleForLesson, setActiveModuleForLesson] = useState(null); + const [lessonForm, setLessonForm] = useState({ + title: '', videoUrl: '', videoType: 'youtube' as 'youtube' | 'direct', order: 1, content: '' + }); + + useEffect(() => { + fetchCourses(); + }, []); + + const fetchCourses = async () => { + setIsLoading(true); + try { + const res = await fetch('/api/courses', { + headers: { 'Authorization': `Bearer ${token}` } + }); + if (res.ok) { + const data = await res.json(); + setCourses(data); + } + } catch (err) { + console.error('Error fetching courses:', err); + } finally { + setIsLoading(false); + } + }; + + const fetchCourseModulesAndLessons = async (courseId: string) => { + setIsLoading(true); + try { + const res = await fetch(`/api/courses/${courseId}`, { + headers: { 'Authorization': `Bearer ${token}` } + }); + if (res.ok) { + const data = await res.json(); + setModules(data.modules); + } + } catch (err) { + console.error('Error loading modules:', err); + } finally { + setIsLoading(false); + } + }; + + const handleSelectCourse = (course: Course) => { + setSelectedCourse(course); + fetchCourseModulesAndLessons(course.id); + }; + + // --- COURSE CRUD --- + const handleSaveCourse = async (e: React.FormEvent) => { + e.preventDefault(); + const method = editingCourse ? 'PUT' : 'POST'; + const url = editingCourse ? `/api/admin/courses/${editingCourse.id}` : '/api/admin/courses'; + + try { + const res = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(courseForm) + }); + + if (res.ok) { + await fetchCourses(); + setShowCourseForm(false); + setEditingCourse(null); + setCourseForm({ title: '', description: '', thumbnail: '', category: '', isLocked: false, price: 0 }); + } + } catch (err) { + console.error('Error saving course:', err); + } + }; + + const handleEditCourse = (course: Course) => { + setEditingCourse(course); + setCourseForm({ + title: course.title, + description: course.description, + thumbnail: course.thumbnail, + category: course.category, + isLocked: course.isLocked || false, + price: course.price || 0 + }); + setShowCourseForm(true); + }; + + const handleDeleteCourse = async (courseId: string) => { + if (!window.confirm('Tem certeza que deseja excluir este curso e todo o seu conteúdo (módulos, aulas, anotações)?')) return; + + try { + const res = await fetch(`/api/admin/courses/${courseId}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` } + }); + + if (res.ok) { + fetchCourses(); + if (selectedCourse?.id === courseId) { + setSelectedCourse(null); + setModules([]); + } + } + } catch (err) { + console.error('Error deleting course:', err); + } + }; + + // --- MODULE CRUD --- + const handleSaveModule = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedCourse) return; + + const method = editingModule ? 'PUT' : 'POST'; + const url = editingModule ? `/api/admin/modules/${editingModule.id}` : '/api/admin/modules'; + const payload = editingModule ? { ...moduleForm } : { ...moduleForm, courseId: selectedCourse.id }; + + try { + const res = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(payload) + }); + + if (res.ok) { + await fetchCourseModulesAndLessons(selectedCourse.id); + setShowModuleForm(false); + setEditingModule(null); + setModuleForm({ title: '', order: 1 }); + } + } catch (err) { + console.error('Error saving module:', err); + } + }; + + const handleEditModule = (mod: Module) => { + setEditingModule(mod); + setModuleForm({ title: mod.title, order: mod.order }); + setShowModuleForm(true); + }; + + const handleDeleteModule = async (modId: string) => { + if (!window.confirm('Excluir este módulo apagará todas as suas aulas associadas. Continuar?')) return; + if (!selectedCourse) return; + + try { + const res = await fetch(`/api/admin/modules/${modId}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` } + }); + + if (res.ok) { + fetchCourseModulesAndLessons(selectedCourse.id); + } + } catch (err) { + console.error('Error deleting module:', err); + } + }; + + // --- LESSON CRUD --- + const handleSaveLesson = async (e: React.FormEvent) => { + e.preventDefault(); + if (!selectedCourse) return; + + const method = editingLesson ? 'PUT' : 'POST'; + const url = editingLesson ? `/api/admin/lessons/${editingLesson.id}` : '/api/admin/lessons'; + const payload = editingLesson ? { ...lessonForm } : { ...lessonForm, moduleId: activeModuleForLesson }; + + try { + const res = await fetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(payload) + }); + + if (res.ok) { + await fetchCourseModulesAndLessons(selectedCourse.id); + setShowLessonForm(false); + setEditingLesson(null); + setActiveModuleForLesson(null); + setLessonForm({ title: '', videoUrl: '', videoType: 'youtube', order: 1, content: '' }); + } + } catch (err) { + console.error('Error saving lesson:', err); + } + }; + + const handleNewLesson = (moduleId: string, lessonsCount: number) => { + setActiveModuleForLesson(moduleId); + setEditingLesson(null); + setLessonForm({ + title: '', + videoUrl: '', + videoType: 'youtube', + order: lessonsCount + 1, + content: '' + }); + setShowLessonForm(true); + }; + + const handleEditLesson = (lesson: Lesson) => { + setEditingLesson(lesson); + setLessonForm({ + title: lesson.title, + videoUrl: lesson.videoUrl, + videoType: lesson.videoType, + order: lesson.order, + content: lesson.content + }); + setShowLessonForm(true); + }; + + const handleDeleteLesson = async (lessonId: string) => { + if (!window.confirm('Excluir esta aula?')) return; + if (!selectedCourse) return; + + try { + const res = await fetch(`/api/admin/lessons/${lessonId}`, { + method: 'DELETE', + headers: { 'Authorization': `Bearer ${token}` } + }); + + if (res.ok) { + fetchCourseModulesAndLessons(selectedCourse.id); + } + } catch (err) { + console.error('Error deleting lesson:', err); + } + }; + + if (isLoading && courses.length === 0) { + return ( +
+ +

Carregando catálogo de treinamentos...

+
+ ); + } + + return ( +
+ {/* Top Header */} +
+
+

+ + Gestão de Cursos, Módulos e Aulas +

+

Crie cursos, adicione módulos e configure links de vídeo (YouTube ou Upload Direto).

+
+ {!selectedCourse && !showCourseForm && ( + + )} +
+ + {/* --- RENDER COURSE FORM --- */} + {showCourseForm && ( +
+
+

+ {editingCourse ? 'Editar Detalhes do Curso' : 'Criar Novo Curso'} +

+ +
+ +
+
+ + setCourseForm({ ...courseForm, title: e.target.value })} + placeholder="Ex: Infraestrutura de Servidores Linux" + className="w-full glass-input rounded-lg p-3 text-sm text-white focus:outline-none" + /> +
+ +
+ + setCourseForm({ ...courseForm, category: e.target.value })} + placeholder="Ex: Redes e Servidores" + className="w-full glass-input rounded-lg p-3 text-sm text-white focus:outline-none" + /> +
+ +
+ + setCourseForm({ ...courseForm, thumbnail: e.target.value })} + placeholder="https://images.unsplash.com/... ou link de imagem direta" + className="w-full glass-input rounded-lg p-3 text-sm text-white focus:outline-none" + /> +
+ +
+ + setCourseForm({ ...courseForm, price: Number(e.target.value) })} + placeholder="Ex: 89.90" + className="w-full glass-input rounded-lg p-3 text-sm text-white focus:outline-none font-mono" + /> +
+ +
+ + +
+ +
+ +