From 0cc3bb7c7a94329319ff7b938dc5b1b812f0c77f Mon Sep 17 00:00:00 2001 From: Tiago Yamamoto Date: Sat, 27 Dec 2025 13:35:03 -0300 Subject: [PATCH] Add multi-tenant Appwrite backend --- .env.example | 18 - .gitignore | 3 + README.md | 127 +- backend/.env.example | 18 + backend/Dockerfile | 17 + backend/data/.gitkeep | 0 backend/docker-compose.yml | 11 + backend/package-lock.json | 1587 +++++++++++++++++ backend/package.json | 26 + backend/server.js | 108 -- .../src/config/appwrite.json | 8 +- backend/src/docs/BACKEND.md | 30 + {docs => backend/src/docs}/SECURITY.md | 0 backend/src/docs/SETUP_GUIDE.md | 23 + backend/src/docs/appwrite-databases-schema.md | 46 + backend/src/lib/appwrite.ts | 184 ++ backend/src/lib/env.ts | 30 + backend/src/lib/logger.ts | 15 + backend/src/main.ts | 34 + backend/src/modules/auth/auth.middleware.ts | 22 + backend/src/modules/finops/finops.service.ts | 24 + .../src/modules/projects/appwrite.routes.ts | 36 + .../src/modules/projects/appwrite.service.ts | 56 + .../src/modules/projects/appwriteSchema.ts | 179 ++ backend/src/modules/tenants/tenants.routes.ts | 85 + .../src/modules/tenants/tenants.service.ts | 165 ++ backend/src/modules/tenants/tenants.store.ts | 27 + backend/src/modules/tenants/tenants.types.ts | 35 + backend/src/scripts/setup-appwrite.ts | 28 + backend/tsconfig.json | 15 + docs/BACKEND.md | 43 - docs/SETUP_GUIDE.md | 172 -- docs/appwrite-databases-schema.md | 91 - package-lock.json | 637 +------ package.json | 10 +- setup-appwrite.js | 406 ----- 36 files changed, 2810 insertions(+), 1506 deletions(-) delete mode 100644 .env.example create mode 100644 backend/.env.example create mode 100644 backend/Dockerfile create mode 100644 backend/data/.gitkeep create mode 100644 backend/docker-compose.yml create mode 100644 backend/package-lock.json create mode 100644 backend/package.json delete mode 100644 backend/server.js rename appwrite.json => backend/src/config/appwrite.json (50%) create mode 100644 backend/src/docs/BACKEND.md rename {docs => backend/src/docs}/SECURITY.md (100%) create mode 100644 backend/src/docs/SETUP_GUIDE.md create mode 100644 backend/src/docs/appwrite-databases-schema.md create mode 100644 backend/src/lib/appwrite.ts create mode 100644 backend/src/lib/env.ts create mode 100644 backend/src/lib/logger.ts create mode 100644 backend/src/main.ts create mode 100644 backend/src/modules/auth/auth.middleware.ts create mode 100644 backend/src/modules/finops/finops.service.ts create mode 100644 backend/src/modules/projects/appwrite.routes.ts create mode 100644 backend/src/modules/projects/appwrite.service.ts create mode 100644 backend/src/modules/projects/appwriteSchema.ts create mode 100644 backend/src/modules/tenants/tenants.routes.ts create mode 100644 backend/src/modules/tenants/tenants.service.ts create mode 100644 backend/src/modules/tenants/tenants.store.ts create mode 100644 backend/src/modules/tenants/tenants.types.ts create mode 100644 backend/src/scripts/setup-appwrite.ts create mode 100644 backend/tsconfig.json delete mode 100644 docs/BACKEND.md delete mode 100644 docs/SETUP_GUIDE.md delete mode 100644 docs/appwrite-databases-schema.md delete mode 100644 setup-appwrite.js diff --git a/.env.example b/.env.example deleted file mode 100644 index 3ef51cc..0000000 --- a/.env.example +++ /dev/null @@ -1,18 +0,0 @@ -APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 -APPWRITE_PROJECT_ID= -APPWRITE_API_KEY= -APPWRITE_DATABASE_ID= -APPWRITE_COLLECTION_SERVERS_ID= -APPWRITE_COLLECTION_GITHUB_REPOS_ID= -APPWRITE_COLLECTION_AUDIT_LOGS_ID= -APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID= -BACKEND_PORT=4000 -APPWRITE_FUNCTIONS_ENDPOINT= -APPWRITE_FUNCTIONS_API_KEY= -VITE_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 -VITE_APPWRITE_PROJECT_ID= -VITE_APPWRITE_DATABASE_ID= -VITE_APPWRITE_COLLECTION_SERVERS_ID= -VITE_APPWRITE_COLLECTION_GITHUB_REPOS_ID= -VITE_APPWRITE_COLLECTION_AUDIT_LOGS_ID= -VITE_APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID= diff --git a/.gitignore b/.gitignore index 175ef63..c5b8058 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ Thumbs.db # Build outputs dist/ build/ + +# Backend data +backend/data/tenants.json diff --git a/README.md b/README.md index 2c4c555..822c91d 100644 --- a/README.md +++ b/README.md @@ -19,18 +19,55 @@ ## 🎯 Visão Geral -Este monorepo contém três componentes principais: +Este monorepo contém quatro componentes principais: - **Landing Page**: Interface pública desenvolvida com Fresh (framework Deno) e Tailwind CSS - **Dashboard**: Painel administrativo em React + TypeScript + Vite com integração Appwrite - **Appwrite Functions**: Três funções serverless (hello-world, sync-github, check-cloudflare-status) +- **Backend (Node.js + TypeScript)**: API administrativa multi-tenant para gerenciar projetos Appwrite -**Backend**: Appwrite Cloud - BaaS (Backend as a Service) com: +**Infra principal Appwrite** (por projeto): - Autenticação (Email/Password) - Database com 4 coleções (servers, github_repos, audit_logs, cloud_accounts) - Realtime subscriptions para logs ao vivo - Functions para automação +## 🧱 Arquitetura do Backend Multi-tenant + +- **Tenants** representam empresas/clientes. +- Cada tenant possui **um ou mais projetos Appwrite** com endpoint + API key próprios. +- O backend persiste o controle local em `backend/data/tenants.json`. +- Autenticação administrativa via `ADMIN_API_TOKEN`. + +## 🔁 Fluxo multi-tenant (resumo) + +1. `POST /tenants` cria o tenant. +2. `POST /tenants/:id/appwrite-project` registra o projeto Appwrite do tenant. +3. `POST /appwrite/setup` aplica schema base automaticamente. +4. `GET /tenants/:id/appwrite-projects` lista projetos vinculados. + +### Como adicionar um novo Appwrite (exemplo rápido) + +```bash +# 1) Criar tenant +curl -X POST http://localhost:4000/tenants \\ + -H \"Authorization: Bearer \" \\ + -H \"Content-Type: application/json\" \\ + -d '{\"name\":\"Acme Corp\"}' + +# 2) Registrar projeto Appwrite do tenant +curl -X POST http://localhost:4000/tenants//appwrite-project \\ + -H \"Authorization: Bearer \" \\ + -H \"Content-Type: application/json\" \\ + -d '{\"name\":\"Acme Project\",\"endpoint\":\"https://cloud.appwrite.io/v1\",\"projectId\":\"\",\"apiKey\":\"\"}' + +# 3) Aplicar schema base +curl -X POST http://localhost:4000/appwrite/setup \\ + -H \"Authorization: Bearer \" \\ + -H \"Content-Type: application/json\" \\ + -d '{\"tenantId\":\"\",\"projectRef\":\"\"}' +``` + ## 🛠 Pré-requisitos Certifique-se de ter instalado: @@ -60,59 +97,58 @@ cd core # 2. Instale as dependências raiz npm install -# 3. Instale as dependências do dashboard +# 3. Instale as dependências do backend +cd backend +npm install +cd .. + +# 4. Instale as dependências do dashboard cd dashboard npm install cd .. -# 4. Verifique se o Deno está instalado +# 5. Verifique se o Deno está instalado deno --version # Se não estiver, instale: curl -fsSL https://deno.land/install.sh | sh -# 5. Configure as variáveis de ambiente -cp .env.example .env -# Edite o .env com suas credenciais Appwrite (veja seção abaixo) +# 6. Configure as variáveis de ambiente +cp backend/.env.example backend/.env +# Edite o backend/.env com suas credenciais Appwrite (veja seção abaixo) -# 6. Configure o Appwrite Cloud (veja seção "Setup Appwrite Cloud") +# 7. Configure o Appwrite Cloud (veja seção "Setup Appwrite Cloud") -# 7. Execute o projeto +# 8. Execute o projeto npm run dev:web +npm run dev:backend ``` ## ⚙️ Configuração Detalhada ### Variáveis de Ambiente -O arquivo `.env` na raiz do projeto contém todas as configurações necessárias. Copie o `.env.example` e preencha os valores: +O backend usa um arquivo `.env` próprio em `backend/.env`. Copie o `.env.example` do backend e preencha os valores: ```bash -cp .env.example .env +cp backend/.env.example backend/.env ``` -#### Referência Completa de Variáveis +#### Referência Completa de Variáveis (Backend) -| Variável | Onde Obter | Obrigatória | Descrição | -|----------|------------|-------------|-----------| -| **Configuração Server-Side (Scripts Node.js e Functions)** | | | | -| `APPWRITE_ENDPOINT` | Fixo | ✅ | URL da API Appwrite. Use `https://cloud.appwrite.io/v1` | -| `APPWRITE_PROJECT_ID` | Console Appwrite | ✅ | ID do projeto. Obtido em: Dashboard → Seu Projeto → Settings | -| `APPWRITE_API_KEY` | Console Appwrite | ✅ | Chave API com permissões Admin. Criar em: Settings → API Keys → Create API Key → Selecione todos os scopes | -| `APPWRITE_FUNCTIONS_ENDPOINT` | Opcional | ❌ | Endpoint customizado para Functions. Deixe vazio para usar o mesmo do `APPWRITE_ENDPOINT` | -| `APPWRITE_FUNCTIONS_API_KEY` | Opcional | ❌ | API Key separada para Functions. Deixe vazio para usar `APPWRITE_API_KEY` | -| **Configuração Client-Side (React Dashboard - Vite)** | | | | -| `VITE_APPWRITE_ENDPOINT` | Fixo | ✅ | Mesmo que `APPWRITE_ENDPOINT`. Prefixo `VITE_` expõe no browser | -| `VITE_APPWRITE_PROJECT_ID` | Console Appwrite | ✅ | Mesmo que `APPWRITE_PROJECT_ID` | -| `VITE_APPWRITE_DATABASE_ID` | Console Appwrite | ✅ | ID do Database criado. Obtido em: Databases → Seu Database → Settings | -| `VITE_APPWRITE_COLLECTION_SERVERS_ID` | Console Appwrite | ✅ | ID da coleção `servers`. Obtido em: Databases → Collections → servers → Settings | -| `VITE_APPWRITE_COLLECTION_GITHUB_REPOS_ID` | Console Appwrite | ✅ | ID da coleção `github_repos` | -| `VITE_APPWRITE_COLLECTION_AUDIT_LOGS_ID` | Console Appwrite | ✅ | ID da coleção `audit_logs` (usado para Realtime no terminal) | -| `VITE_APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID` | Console Appwrite | ✅ | ID da coleção `cloud_accounts` ou `cloudflare_accounts` | +| Variável | Obrigatória | Descrição | +|----------|-------------|-----------| +| `ADMIN_API_TOKEN` | ✅ | Token de acesso administrativo (`Authorization: Bearer ...`) | +| `APPWRITE_ADMIN_ENDPOINT` | ✅ | Endpoint Appwrite para criar projetos | +| `APPWRITE_ADMIN_PROJECT_ID` | ✅ | Project ID administrativo (normalmente `console`) | +| `APPWRITE_ADMIN_API_KEY` | ✅ | API Key com permissões admin para projetos | +| `DEFAULT_APPWRITE_ENDPOINT` | ✅ | Endpoint padrão para novos tenants | +| `APPWRITE_DEFAULT_RUNTIME` | ❌ | Runtime padrão das Functions (ex: `deno-1.35`) | +| `DATA_DIR` | ❌ | Diretório local para persistir tenants (default: `./data`) | -**⚠️ IMPORTANTE**: Variáveis com prefixo `VITE_` são expostas no JavaScript do browser. **NUNCA** coloque informações sensíveis (API Keys) nelas! +> As variáveis client-side do Dashboard permanecem em seu próprio `.env` (prefixo `VITE_`). **Nunca** use API keys no front-end. ### Setup Appwrite Cloud -Siga este passo a passo para configurar o backend: +Siga este passo a passo para configurar o Appwrite usado pelo dashboard. O backend multi-tenant aplica o próprio schema via `POST /appwrite/setup` quando necessário. #### 1. Criar Projeto @@ -279,6 +315,13 @@ npm run dev:dashboard ``` Acesse: http://localhost:5173 +**Backend (Node.js + TypeScript):** +```bash +npm run dev:backend +# Ou: cd backend && npm run dev +``` +Acesse: http://localhost:4000 + Login no dashboard usa as credenciais criadas no Appwrite (ex: `admin@test.com` / `admin123`). ### Build para Produção @@ -300,13 +343,21 @@ deno task build ``` core/ -├── .env # Variáveis de ambiente (NÃO commitar!) -├── .env.example # Template de variáveis ├── package.json # Scripts raiz e npm-run-all ├── README.md # 📄 Este arquivo -├── SECURITY.md # Política de segurança -├── appwrite.json # Configuração Appwrite CLI -├── appwrite-databases-schema.md # Schema detalhado do banco +├── backend/ # 🧠 API administrativa (Node + TS) +│ ├── src/ +│ │ ├── modules/ # Tenants, projects, auth, finops +│ │ ├── lib/ # Appwrite SDK, env, logger +│ │ ├── scripts/ # Setup Appwrite +│ │ ├── config/ # appwrite.json +│ │ ├── docs/ # Docs backend +│ │ └── main.ts # Entry point +│ ├── Dockerfile +│ ├── docker-compose.yml +│ ├── package.json +│ ├── tsconfig.json +│ └── .env.example │ ├── dashboard/ # 🎨 Painel React + Vite │ ├── src/ @@ -342,10 +393,12 @@ core/ | Script | Comando | Descrição | |--------|---------|-----------| +| `dev:backend` | `npm run dev:backend` | Inicia a API backend multi-tenant | | `dev:dashboard` | `npm run dev:dashboard` | Inicia somente o dashboard | | `dev:landing` | `npm run dev:landing` | Inicia somente a landing | | `dev:web` | `npm run dev:web` | Inicia dashboard + landing em paralelo | | `lint:dashboard` | `npm run lint:dashboard` | Executa ESLint no dashboard | +| `setup:appwrite` | `npm run setup:appwrite` | Aplica o schema base Appwrite | ### Dashboard (`cd dashboard`) @@ -397,7 +450,7 @@ deno task check # Deve passar formatting, linting e type-check # 6. Verificar arquivo .env -cat ../.env +cat backend/.env # Deve ter todos os IDs preenchidos (não vazios) ``` @@ -539,7 +592,7 @@ Ou manualmente via Appwrite Console → Functions. - **NUNCA** commite o arquivo `.env` - API Keys devem ter scopes mínimos necessários em produção - Habilite MFA no Appwrite Console -- Revise `SECURITY.md` para reportar vulnerabilidades +- Revise `backend/src/docs/SECURITY.md` para reportar vulnerabilidades ## 📝 Licença diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..c691894 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,18 @@ +NODE_ENV=development +BACKEND_PORT=4000 +ADMIN_API_TOKEN=change-me +DATA_DIR=./data + +# Appwrite admin credentials (to create projects) +APPWRITE_ADMIN_ENDPOINT=https://cloud.appwrite.io/v1 +APPWRITE_ADMIN_PROJECT_ID=console +APPWRITE_ADMIN_API_KEY= + +# Defaults for new tenant projects +DEFAULT_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 +APPWRITE_DEFAULT_RUNTIME=deno-1.35 + +# Optional setup script target +APPWRITE_SETUP_ENDPOINT= +APPWRITE_SETUP_PROJECT_ID= +APPWRITE_SETUP_API_KEY= diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..cab32dc --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,17 @@ +FROM node:20-alpine AS base + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm install + +COPY tsconfig.json ./ +COPY src ./src + +RUN npm run build + +RUN npm prune --omit=dev + +EXPOSE 4000 + +CMD ["node", "dist/main.js"] diff --git a/backend/data/.gitkeep b/backend/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..25e6d80 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3.9' + +services: + backend: + build: . + ports: + - "4000:4000" + env_file: + - .env + volumes: + - ./data:/app/data diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..c44f05b --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,1587 @@ +{ + "name": "core-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "core-backend", + "version": "1.0.0", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "node-appwrite": "^14.1.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/node": "^20.12.12", + "tsx": "^4.15.7", + "typescript": "^5.4.5" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "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.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "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.14.0", + "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" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-appwrite": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-14.2.0.tgz", + "integrity": "sha512-sPPA+JzdBJRS+lM6azX85y3/6iyKQYlHcXCbjMuWLROh6IiU9EfXRW3XSUTa5HDoBrlo8ve+AnVA6BIjQfUs1g==", + "license": "BSD-3-Clause", + "dependencies": { + "node-fetch-native-with-agent": "1.7.2" + } + }, + "node_modules/node-fetch-native-with-agent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-fetch-native-with-agent/-/node-fetch-native-with-agent-1.7.2.tgz", + "integrity": "sha512-5MaOOCuJEvcckoz7/tjdx1M6OusOY6Xc5f459IaruGStWnKzlI1qpNgaAwmn4LmFYcsSlj+jBMk84wmmRxfk5g==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..990404d --- /dev/null +++ b/backend/package.json @@ -0,0 +1,26 @@ +{ + "name": "core-backend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "tsx watch src/main.ts", + "build": "tsc", + "start": "node dist/main.js", + "setup:appwrite": "tsx src/scripts/setup-appwrite.ts" + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "node-appwrite": "^14.1.0", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/node": "^20.12.12", + "tsx": "^4.15.7", + "typescript": "^5.4.5" + } +} diff --git a/backend/server.js b/backend/server.js deleted file mode 100644 index b8cb37f..0000000 --- a/backend/server.js +++ /dev/null @@ -1,108 +0,0 @@ -import cors from 'cors'; -import dotenv from 'dotenv'; -import express from 'express'; -import { Client, Databases, Query } from 'node-appwrite'; - -dotenv.config(); - -const app = express(); - -app.use(cors()); -app.use(express.json()); - -const requiredEnv = ['APPWRITE_ENDPOINT', 'APPWRITE_PROJECT_ID', 'APPWRITE_API_KEY']; -const missingEnv = requiredEnv.filter((key) => !process.env[key]); - -const databaseId = - process.env.APPWRITE_DATABASE_ID || process.env.VITE_APPWRITE_DATABASE_ID || ''; - -const collectionIds = { - servers: - process.env.APPWRITE_COLLECTION_SERVERS_ID || - process.env.VITE_APPWRITE_COLLECTION_SERVERS_ID || - '', - githubRepos: - process.env.APPWRITE_COLLECTION_GITHUB_REPOS_ID || - process.env.VITE_APPWRITE_COLLECTION_GITHUB_REPOS_ID || - '', - auditLogs: - process.env.APPWRITE_COLLECTION_AUDIT_LOGS_ID || - process.env.VITE_APPWRITE_COLLECTION_AUDIT_LOGS_ID || - '', - cloudAccounts: - process.env.APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID || - process.env.VITE_APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID || - '', -}; - -const client = new Client() - .setEndpoint(process.env.APPWRITE_ENDPOINT || '') - .setProject(process.env.APPWRITE_PROJECT_ID || '') - .setKey(process.env.APPWRITE_API_KEY || ''); - -const databases = new Databases(client); - -const getPaginationQueries = (request) => { - const limit = Number.parseInt(request.query.limit ?? '25', 10); - const offset = Number.parseInt(request.query.offset ?? '0', 10); - - const queries = []; - - if (!Number.isNaN(limit)) { - queries.push(Query.limit(Math.min(Math.max(limit, 1), 100))); - } - - if (!Number.isNaN(offset)) { - queries.push(Query.offset(Math.max(offset, 0))); - } - - return queries; -}; - -const listCollection = (collectionId, extraQueries = []) => async (request, response) => { - if (missingEnv.length > 0) { - return response.status(500).json({ - error: 'Missing required environment variables.', - missing: missingEnv, - }); - } - - if (!databaseId || !collectionId) { - return response.status(500).json({ - error: 'Missing Appwrite database or collection configuration.', - databaseId, - collectionId, - }); - } - - try { - const documents = await databases.listDocuments( - databaseId, - collectionId, - [...getPaginationQueries(request), ...extraQueries] - ); - - return response.json(documents); - } catch (error) { - return response.status(500).json({ - error: 'Failed to fetch Appwrite documents.', - details: error.message, - }); - } -}; - -app.get('/health', (request, response) => { - response.json({ status: 'ok' }); -}); - -app.get('/servers', listCollection(collectionIds.servers)); -app.get('/github-repos', listCollection(collectionIds.githubRepos)); -app.get('/cloud-accounts', listCollection(collectionIds.cloudAccounts)); -app.get('/audit-logs', listCollection(collectionIds.auditLogs, [Query.orderDesc('timestamp')])); - -const port = Number.parseInt(process.env.BACKEND_PORT ?? '4000', 10); - -app.listen(port, () => { - // eslint-disable-next-line no-console - console.log(`Backend listening on http://localhost:${port}`); -}); diff --git a/appwrite.json b/backend/src/config/appwrite.json similarity index 50% rename from appwrite.json rename to backend/src/config/appwrite.json index 20b1d59..b06b777 100644 --- a/appwrite.json +++ b/backend/src/config/appwrite.json @@ -1,11 +1,11 @@ { "provider": "appwrite-cloud", "description": "Appwrite Cloud configuration for DevOps orchestration platform", - "endpoint": "${APPWRITE_ENDPOINT}", - "projectId": "${APPWRITE_PROJECT_ID}", - "apiKey": "${APPWRITE_API_KEY}", + "endpoint": "${APPWRITE_ADMIN_ENDPOINT}", + "projectId": "${APPWRITE_ADMIN_PROJECT_ID}", + "apiKey": "${APPWRITE_ADMIN_API_KEY}", "functions": { "defaultRuntime": "deno-1.35", - "source": "./appwrite-functions" + "source": "../../appwrite-functions" } } diff --git a/backend/src/docs/BACKEND.md b/backend/src/docs/BACKEND.md new file mode 100644 index 0000000..2282995 --- /dev/null +++ b/backend/src/docs/BACKEND.md @@ -0,0 +1,30 @@ +# Backend multi-tenant Appwrite + +Este backend é a camada administrativa para gerenciar múltiplos projetos Appwrite. + +## O que ele faz + +- CRUD de tenants e projetos Appwrite (multi-tenant). +- Setup e sincronização automática de schema (database, collections, buckets e functions). +- Base para FinOps (métricas de uso) e automações DevOps. + +## Rodando localmente + +```bash +cd backend +npm install +cp .env.example .env +npm run dev +``` + +A API inicia em `http://localhost:4000`. + +## Endpoints mínimos + +- `POST /tenants` +- `GET /tenants` +- `POST /tenants/:id/appwrite-project` +- `GET /tenants/:id/appwrite-projects` +- `POST /appwrite/setup` +- `POST /appwrite/sync-schema` +- `GET /health` diff --git a/docs/SECURITY.md b/backend/src/docs/SECURITY.md similarity index 100% rename from docs/SECURITY.md rename to backend/src/docs/SECURITY.md diff --git a/backend/src/docs/SETUP_GUIDE.md b/backend/src/docs/SETUP_GUIDE.md new file mode 100644 index 0000000..0402741 --- /dev/null +++ b/backend/src/docs/SETUP_GUIDE.md @@ -0,0 +1,23 @@ +# Guia rápido - Setup Appwrite (Multi-tenant) + +O script `setup-appwrite.ts` aplica o schema base (database, collections, buckets e functions) +para um projeto Appwrite específico. + +## Variáveis necessárias + +Edite `backend/.env` e configure: + +```env +APPWRITE_SETUP_ENDPOINT=https://cloud.appwrite.io/v1 +APPWRITE_SETUP_PROJECT_ID= +APPWRITE_SETUP_API_KEY= +``` + +## Executar + +```bash +cd backend +npm run setup:appwrite +``` + +O script é idempotente (pode rodar várias vezes sem duplicar recursos). diff --git a/backend/src/docs/appwrite-databases-schema.md b/backend/src/docs/appwrite-databases-schema.md new file mode 100644 index 0000000..a4c9e61 --- /dev/null +++ b/backend/src/docs/appwrite-databases-schema.md @@ -0,0 +1,46 @@ +# Appwrite Database Schema (Base Multi-tenant) + +Schema base aplicado pelo backend para novos projetos Appwrite. + +## Database + +- **ID**: `core-platform` +- **Nome**: Core Platform + +## Collections + +### tenants +- `name` (string) +- `slug` (string) +- `status` (enum: active | inactive) +- `createdAt` (datetime) + +### projects +- `tenantId` (string) +- `name` (string) +- `endpoint` (string) +- `projectId` (string) +- `createdAt` (datetime) + +### finops_usage +- `tenantId` (string) +- `users` (integer) +- `documents` (integer) +- `storageBytes` (integer) +- `functionRuns` (integer) +- `capturedAt` (datetime) + +### audit_logs +- `event` (string) +- `actor` (string) +- `timestamp` (datetime) + +## Buckets + +- `tenant-assets` (storage base por tenant) + +## Functions + +- `hello-world` +- `sync-github` +- `check-cloudflare-status` diff --git a/backend/src/lib/appwrite.ts b/backend/src/lib/appwrite.ts new file mode 100644 index 0000000..4758920 --- /dev/null +++ b/backend/src/lib/appwrite.ts @@ -0,0 +1,184 @@ +import { + Client, + Databases, + Functions, + Storage, + ID, + Runtime, +} from 'node-appwrite'; +import { appwriteSchema } from '../modules/projects/appwriteSchema.js'; +import { logger } from './logger.js'; + +export type AppwriteConnection = { + endpoint: string; + projectId: string; + apiKey: string; +}; + +export type ApplySchemaResult = { + databaseId: string; + collections: string[]; + buckets: string[]; + functions: string[]; +}; + +export const createAppwriteClient = ({ endpoint, projectId, apiKey }: AppwriteConnection) => + new Client().setEndpoint(endpoint).setProject(projectId).setKey(apiKey); + +const isConflict = (error: unknown) => + typeof error === 'object' && error !== null && 'code' in error && (error as { code?: number }).code === 409; + +export const applySchema = async ( + connection: AppwriteConnection, + options?: { logPrefix?: string } +): Promise => { + const client = createAppwriteClient(connection); + const databases = new Databases(client); + const storage = new Storage(client); + const functions = new Functions(client); + const logPrefix = options?.logPrefix ?? 'appwrite'; + + logger.info(`[${logPrefix}] Applying Appwrite schema`, { + endpoint: connection.endpoint, + projectId: connection.projectId, + }); + + const { database } = appwriteSchema; + + try { + await databases.create(database.id, database.name, database.enabled); + logger.info(`[${logPrefix}] Database created`, { databaseId: database.id }); + } catch (error) { + if (!isConflict(error)) { + throw error; + } + logger.info(`[${logPrefix}] Database already exists`, { databaseId: database.id }); + } + + for (const collection of appwriteSchema.collections) { + try { + await databases.createCollection(database.id, collection.id, collection.name, collection.permissions); + logger.info(`[${logPrefix}] Collection created`, { collectionId: collection.id }); + } catch (error) { + if (!isConflict(error)) { + throw error; + } + logger.info(`[${logPrefix}] Collection already exists`, { collectionId: collection.id }); + } + + for (const attribute of collection.attributes) { + try { + switch (attribute.type) { + case 'string': + await databases.createStringAttribute( + database.id, + collection.id, + attribute.key, + attribute.size, + attribute.required, + attribute.array ?? false + ); + break; + case 'enum': + await databases.createEnumAttribute( + database.id, + collection.id, + attribute.key, + attribute.elements, + attribute.required + ); + break; + case 'integer': + await databases.createIntegerAttribute( + database.id, + collection.id, + attribute.key, + attribute.required + ); + break; + case 'datetime': + await databases.createDatetimeAttribute( + database.id, + collection.id, + attribute.key, + attribute.required + ); + break; + case 'boolean': + await databases.createBooleanAttribute( + database.id, + collection.id, + attribute.key, + attribute.required + ); + break; + case 'url': + await databases.createUrlAttribute( + database.id, + collection.id, + attribute.key, + attribute.required + ); + break; + default: + break; + } + logger.info(`[${logPrefix}] Attribute ensured`, { + collectionId: collection.id, + attribute: attribute.key, + }); + } catch (error) { + if (!isConflict(error)) { + throw error; + } + } + } + } + + for (const bucket of appwriteSchema.buckets) { + try { + await storage.createBucket(bucket.id, bucket.name, bucket.permissions, bucket.fileSecurity ?? false); + logger.info(`[${logPrefix}] Bucket created`, { bucketId: bucket.id }); + } catch (error) { + if (!isConflict(error)) { + throw error; + } + logger.info(`[${logPrefix}] Bucket already exists`, { bucketId: bucket.id }); + } + } + + for (const func of appwriteSchema.functions) { + try { + await functions.get(func.id); + logger.info(`[${logPrefix}] Function already exists`, { functionId: func.id }); + } catch (error) { + const code = typeof error === 'object' && error !== null && 'code' in error ? (error as { code?: number }).code : null; + if (code !== 404 && code !== 409) { + throw error; + } + await functions.create(func.id, func.name, func.runtime as Runtime, func.execute); + logger.info(`[${logPrefix}] Function created`, { functionId: func.id }); + } + } + + return { + databaseId: database.id, + collections: appwriteSchema.collections.map((collection) => collection.id), + buckets: appwriteSchema.buckets.map((bucket) => bucket.id), + functions: appwriteSchema.functions.map((func) => func.id), + }; +}; + +export const createProject = async ( + connection: AppwriteConnection, + projectName: string +): Promise<{ projectId: string }> => { + const client = createAppwriteClient(connection); + const { Projects } = await import('node-appwrite'); + const projects = new Projects(client); + const projectId = ID.unique(); + + await projects.create(projectId, projectName); + + return { projectId }; +}; diff --git a/backend/src/lib/env.ts b/backend/src/lib/env.ts new file mode 100644 index 0000000..f9a0bbe --- /dev/null +++ b/backend/src/lib/env.ts @@ -0,0 +1,30 @@ +import dotenv from 'dotenv'; +import path from 'path'; + +const envFile = process.env.ENV_FILE || path.resolve(process.cwd(), '.env'); + +dotenv.config({ path: envFile }); + +const numberFromEnv = (value: string | undefined, fallback: number) => { + if (!value) { + return fallback; + } + const parsed = Number.parseInt(value, 10); + return Number.isNaN(parsed) ? fallback : parsed; +}; + +export const env = { + nodeEnv: process.env.NODE_ENV ?? 'development', + port: numberFromEnv(process.env.BACKEND_PORT, 4000), + adminToken: process.env.ADMIN_API_TOKEN ?? '', + dataDir: process.env.DATA_DIR ?? 'data', + appwriteAdmin: { + endpoint: process.env.APPWRITE_ADMIN_ENDPOINT ?? '', + projectId: process.env.APPWRITE_ADMIN_PROJECT_ID ?? '', + apiKey: process.env.APPWRITE_ADMIN_API_KEY ?? '', + }, + defaults: { + appwriteEndpoint: process.env.DEFAULT_APPWRITE_ENDPOINT ?? process.env.APPWRITE_DEFAULT_ENDPOINT ?? '', + functionRuntime: process.env.APPWRITE_DEFAULT_RUNTIME ?? 'deno-1.35', + }, +}; diff --git a/backend/src/lib/logger.ts b/backend/src/lib/logger.ts new file mode 100644 index 0000000..c83763b --- /dev/null +++ b/backend/src/lib/logger.ts @@ -0,0 +1,15 @@ +export type LogMeta = Record | undefined; + +const formatMeta = (meta?: LogMeta) => (meta ? ` ${JSON.stringify(meta)}` : ''); + +export const logger = { + info(message: string, meta?: LogMeta) { + console.log(`[INFO] ${message}${formatMeta(meta)}`); + }, + warn(message: string, meta?: LogMeta) { + console.warn(`[WARN] ${message}${formatMeta(meta)}`); + }, + error(message: string, meta?: LogMeta) { + console.error(`[ERROR] ${message}${formatMeta(meta)}`); + }, +}; diff --git a/backend/src/main.ts b/backend/src/main.ts new file mode 100644 index 0000000..26ccd0e --- /dev/null +++ b/backend/src/main.ts @@ -0,0 +1,34 @@ +import express from 'express'; +import cors from 'cors'; +import { env } from './lib/env.js'; +import { logger } from './lib/logger.js'; +import { requireAdminToken } from './modules/auth/auth.middleware.js'; +import { tenantsRouter } from './modules/tenants/tenants.routes.js'; +import { appwriteRouter } from './modules/projects/appwrite.routes.js'; + +const app = express(); + +app.use(cors()); +app.use(express.json({ limit: '1mb' })); + +app.get('/health', (_request, response) => { + response.json({ status: 'ok' }); +}); + +app.use(requireAdminToken); + +app.use('/tenants', tenantsRouter); +app.use('/appwrite', appwriteRouter); + +app.use((error: Error, _request: express.Request, response: express.Response, _next: express.NextFunction) => { + logger.error('Request failed', { message: error.message }); + + const status = error.message.includes('not found') ? 404 : 400; + response.status(status).json({ + error: error.message, + }); +}); + +app.listen(env.port, () => { + logger.info(`Backend listening on http://localhost:${env.port}`); +}); diff --git a/backend/src/modules/auth/auth.middleware.ts b/backend/src/modules/auth/auth.middleware.ts new file mode 100644 index 0000000..6dec529 --- /dev/null +++ b/backend/src/modules/auth/auth.middleware.ts @@ -0,0 +1,22 @@ +import type { NextFunction, Request, Response } from 'express'; +import { env } from '../../lib/env.js'; + +export const requireAdminToken = (request: Request, response: Response, next: NextFunction) => { + if (!env.adminToken) { + return next(); + } + + const header = request.headers.authorization; + const fallbackToken = request.headers['x-admin-token']; + const tokenValue = Array.isArray(fallbackToken) ? fallbackToken[0] : fallbackToken; + const token = header?.startsWith('Bearer ') ? header.slice(7) : tokenValue; + + if (token !== env.adminToken) { + return response.status(401).json({ + error: 'Unauthorized', + message: 'Invalid admin token', + }); + } + + return next(); +}; diff --git a/backend/src/modules/finops/finops.service.ts b/backend/src/modules/finops/finops.service.ts new file mode 100644 index 0000000..df3f7da --- /dev/null +++ b/backend/src/modules/finops/finops.service.ts @@ -0,0 +1,24 @@ +export type FinopsSnapshot = { + users: number; + documents: number; + storageBytes: number; + functionRuns: number; + capturedAt: string; +}; + +export type FinopsSummary = FinopsSnapshot & { + lastSyncedAt?: string; +}; + +export const createEmptyFinops = (): FinopsSummary => ({ + users: 0, + documents: 0, + storageBytes: 0, + functionRuns: 0, + capturedAt: new Date().toISOString(), +}); + +export const recordSnapshot = (summary: FinopsSummary, snapshot: FinopsSnapshot): FinopsSummary => ({ + ...snapshot, + lastSyncedAt: new Date().toISOString(), +}); diff --git a/backend/src/modules/projects/appwrite.routes.ts b/backend/src/modules/projects/appwrite.routes.ts new file mode 100644 index 0000000..29f1716 --- /dev/null +++ b/backend/src/modules/projects/appwrite.routes.ts @@ -0,0 +1,36 @@ +import { Router } from 'express'; +import { z } from 'zod'; +import { syncSchemaForProject } from './appwrite.service.js'; + +const schemaRequest = z.object({ + tenantId: z.string().min(1), + projectRef: z.string().optional(), +}); + +export const appwriteRouter = Router(); + +appwriteRouter.post('/setup', async (request, response, next) => { + try { + const payload = schemaRequest.parse(request.body); + const result = await syncSchemaForProject(payload.tenantId, payload.projectRef); + response.status(200).json({ + message: 'Appwrite setup applied', + result, + }); + } catch (error) { + next(error); + } +}); + +appwriteRouter.post('/sync-schema', async (request, response, next) => { + try { + const payload = schemaRequest.parse(request.body); + const result = await syncSchemaForProject(payload.tenantId, payload.projectRef); + response.status(200).json({ + message: 'Schema synchronized', + result, + }); + } catch (error) { + next(error); + } +}); diff --git a/backend/src/modules/projects/appwrite.service.ts b/backend/src/modules/projects/appwrite.service.ts new file mode 100644 index 0000000..54d95d8 --- /dev/null +++ b/backend/src/modules/projects/appwrite.service.ts @@ -0,0 +1,56 @@ +import { applySchema } from '../../lib/appwrite.js'; +import { loadTenants, saveTenants } from '../tenants/tenants.store.js'; +import type { AppwriteProject } from '../tenants/tenants.types.js'; +import { logger } from '../../lib/logger.js'; + +export const resolveProject = async (tenantId: string, projectRef?: string): Promise => { + const tenants = await loadTenants(); + const tenant = tenants.find((item) => item.id === tenantId); + + if (!tenant) { + throw new Error('Tenant not found'); + } + + if (!projectRef) { + if (tenant.appwriteProjects.length === 1) { + return tenant.appwriteProjects[0]; + } + throw new Error('Multiple projects found. Provide projectRef.'); + } + + const project = tenant.appwriteProjects.find( + (item) => item.id === projectRef || item.projectId === projectRef + ); + + if (!project) { + throw new Error('Project not found'); + } + + return project; +}; + +export const syncSchemaForProject = async (tenantId: string, projectRef?: string) => { + const project = await resolveProject(tenantId, projectRef); + + const result = await applySchema({ + endpoint: project.endpoint, + projectId: project.projectId, + apiKey: project.apiKey, + }); + + project.updatedAt = new Date().toISOString(); + + const tenants = await loadTenants(); + const tenant = tenants.find((item) => item.id === tenantId); + if (tenant) { + tenant.updatedAt = project.updatedAt; + await saveTenants(tenants); + } + + logger.info('Schema synced for project', { + tenantId, + projectId: project.projectId, + }); + + return result; +}; diff --git a/backend/src/modules/projects/appwriteSchema.ts b/backend/src/modules/projects/appwriteSchema.ts new file mode 100644 index 0000000..ee4b1f9 --- /dev/null +++ b/backend/src/modules/projects/appwriteSchema.ts @@ -0,0 +1,179 @@ +import { Permission, Role, Runtime } from 'node-appwrite'; +import { env } from '../../lib/env.js'; + +export type AttributeDefinition = + | { + type: 'string'; + key: string; + size: number; + required: boolean; + array?: boolean; + } + | { + type: 'enum'; + key: string; + elements: string[]; + required: boolean; + } + | { + type: 'integer'; + key: string; + required: boolean; + } + | { + type: 'datetime'; + key: string; + required: boolean; + } + | { + type: 'boolean'; + key: string; + required: boolean; + } + | { + type: 'url'; + key: string; + required: boolean; + }; + +export type CollectionSchema = { + id: string; + name: string; + permissions: string[]; + attributes: AttributeDefinition[]; +}; + +export type BucketSchema = { + id: string; + name: string; + permissions: string[]; + fileSecurity?: boolean; +}; + +export type FunctionSchema = { + id: string; + name: string; + runtime: Runtime; + execute: string[]; +}; + +export type AppwriteSchema = { + database: { + id: string; + name: string; + enabled: boolean; + }; + collections: CollectionSchema[]; + buckets: BucketSchema[]; + functions: FunctionSchema[]; +}; + +const runtime = (Object.values(Runtime) as string[]).includes(env.defaults.functionRuntime) + ? (env.defaults.functionRuntime as Runtime) + : Runtime.Deno135; + +export const appwriteSchema: AppwriteSchema = { + database: { + id: 'core-platform', + name: 'Core Platform', + enabled: true, + }, + collections: [ + { + id: 'tenants', + name: 'Tenants', + permissions: [ + Permission.read(Role.any()), + Permission.create(Role.team('admins')), + Permission.update(Role.team('admins')), + Permission.delete(Role.team('admins')), + ], + attributes: [ + { type: 'string', key: 'name', size: 255, required: true }, + { type: 'string', key: 'slug', size: 120, required: true }, + { type: 'enum', key: 'status', elements: ['active', 'inactive'], required: true }, + { type: 'datetime', key: 'createdAt', required: true }, + ], + }, + { + id: 'projects', + name: 'Projects', + permissions: [ + Permission.read(Role.team('admins')), + Permission.create(Role.team('admins')), + Permission.update(Role.team('admins')), + Permission.delete(Role.team('admins')), + ], + attributes: [ + { type: 'string', key: 'tenantId', size: 80, required: true }, + { type: 'string', key: 'name', size: 255, required: true }, + { type: 'string', key: 'endpoint', size: 255, required: true }, + { type: 'string', key: 'projectId', size: 255, required: true }, + { type: 'datetime', key: 'createdAt', required: true }, + ], + }, + { + id: 'finops_usage', + name: 'FinOps Usage', + permissions: [ + Permission.read(Role.team('admins')), + Permission.create(Role.team('admins')), + Permission.update(Role.team('admins')), + ], + attributes: [ + { type: 'string', key: 'tenantId', size: 80, required: true }, + { type: 'integer', key: 'users', required: true }, + { type: 'integer', key: 'documents', required: true }, + { type: 'integer', key: 'storageBytes', required: true }, + { type: 'integer', key: 'functionRuns', required: true }, + { type: 'datetime', key: 'capturedAt', required: true }, + ], + }, + { + id: 'audit_logs', + name: 'Audit Logs', + permissions: [ + Permission.read(Role.team('admins')), + Permission.create(Role.team('admins')), + ], + attributes: [ + { type: 'string', key: 'event', size: 500, required: true }, + { type: 'string', key: 'actor', size: 255, required: true }, + { type: 'datetime', key: 'timestamp', required: true }, + ], + }, + ], + buckets: [ + { + id: 'tenant-assets', + name: 'Tenant Assets', + permissions: [ + Permission.read(Role.team('admins')), + Permission.create(Role.team('admins')), + Permission.update(Role.team('admins')), + Permission.delete(Role.team('admins')), + ], + fileSecurity: true, + }, + ], + functions: [ + { + id: 'hello-world', + name: 'Hello World', + runtime, + execute: [Role.any()], + }, + { + id: 'sync-github', + name: 'Sync GitHub', + runtime, + execute: [Role.team('admins')], + }, + { + id: 'check-cloudflare-status', + name: 'Check Cloudflare Status', + runtime, + execute: [Role.team('admins')], + }, + ], +}; diff --git a/backend/src/modules/tenants/tenants.routes.ts b/backend/src/modules/tenants/tenants.routes.ts new file mode 100644 index 0000000..7af4b11 --- /dev/null +++ b/backend/src/modules/tenants/tenants.routes.ts @@ -0,0 +1,85 @@ +import { Router } from 'express'; +import { z } from 'zod'; +import { + addAppwriteProject, + createTenant, + listTenantProjects, + listTenants, + redactProject, + removeTenantProject, + updateTenantProject, +} from './tenants.service.js'; + +const tenantSchema = z.object({ + name: z.string().min(2), + slug: z.string().optional(), +}); + +const projectSchema = z.object({ + name: z.string().min(2), + endpoint: z.string().url(), + projectId: z.string().optional(), + apiKey: z.string().min(10), + createProject: z.boolean().optional(), +}); + +const projectUpdateSchema = projectSchema.partial(); + +export const tenantsRouter = Router(); + +tenantsRouter.post('/', async (request, response, next) => { + try { + const payload = tenantSchema.parse(request.body); + const tenant = await createTenant(payload); + response.status(201).json(tenant); + } catch (error) { + next(error); + } +}); + +tenantsRouter.get('/', async (_request, response, next) => { + try { + const tenants = await listTenants(); + response.json(tenants); + } catch (error) { + next(error); + } +}); + +tenantsRouter.post('/:id/appwrite-project', async (request, response, next) => { + try { + const payload = projectSchema.parse(request.body); + const project = await addAppwriteProject(request.params.id, payload); + response.status(201).json(redactProject(project)); + } catch (error) { + next(error); + } +}); + +tenantsRouter.get('/:id/appwrite-projects', async (request, response, next) => { + try { + const projects = await listTenantProjects(request.params.id); + response.json(projects); + } catch (error) { + next(error); + } +}); + +tenantsRouter.patch('/:id/appwrite-projects/:projectRef', async (request, response, next) => { + try { + const payload = projectUpdateSchema.parse(request.body); + const project = await updateTenantProject(request.params.id, request.params.projectRef, payload); + response.json(redactProject(project)); + } catch (error) { + next(error); + } +}); + +tenantsRouter.delete('/:id/appwrite-projects/:projectRef', async (request, response, next) => { + try { + await removeTenantProject(request.params.id, request.params.projectRef); + response.status(204).send(); + } catch (error) { + next(error); + } +}); diff --git a/backend/src/modules/tenants/tenants.service.ts b/backend/src/modules/tenants/tenants.service.ts new file mode 100644 index 0000000..57eeef8 --- /dev/null +++ b/backend/src/modules/tenants/tenants.service.ts @@ -0,0 +1,165 @@ +import crypto from 'crypto'; +import { createEmptyFinops } from '../finops/finops.service.js'; +import { loadTenants, saveTenants } from './tenants.store.js'; +import type { AppwriteProject, AppwriteProjectInput, Tenant, TenantInput } from './tenants.types.js'; +import { env } from '../../lib/env.js'; +import { createProject } from '../../lib/appwrite.js'; + +const slugify = (name: string) => + name + .toLowerCase() + .normalize('NFD') + .replace(/[^\w\s-]/g, '') + .trim() + .replace(/[\s_-]+/g, '-') + .replace(/^-+|-+$/g, ''); + +export const redactProject = (project: AppwriteProject) => ({ + ...project, + apiKey: project.apiKey ? `${project.apiKey.slice(0, 4)}****${project.apiKey.slice(-4)}` : '', +}); + +export const listTenants = async () => { + const tenants = await loadTenants(); + return tenants.map((tenant) => ({ + ...tenant, + appwriteProjects: tenant.appwriteProjects.map(redactProject), + })); +}; + +export const createTenant = async (input: TenantInput): Promise => { + const tenants = await loadTenants(); + const slug = input.slug ? slugify(input.slug) : slugify(input.name); + + if (tenants.some((tenant) => tenant.slug === slug)) { + throw new Error('Tenant slug already exists'); + } + + const now = new Date().toISOString(); + const tenant: Tenant = { + id: crypto.randomUUID(), + name: input.name, + slug, + status: 'active', + createdAt: now, + updatedAt: now, + appwriteProjects: [], + finops: createEmptyFinops(), + }; + + tenants.push(tenant); + await saveTenants(tenants); + + return tenant; +}; + +export const addAppwriteProject = async (tenantId: string, input: AppwriteProjectInput): Promise => { + const tenants = await loadTenants(); + const tenant = tenants.find((item) => item.id === tenantId); + + if (!tenant) { + throw new Error('Tenant not found'); + } + + let projectId = input.projectId; + + if (input.createProject) { + if (!env.appwriteAdmin.endpoint || !env.appwriteAdmin.apiKey || !env.appwriteAdmin.projectId) { + throw new Error('Missing Appwrite admin credentials to create projects'); + } + + const created = await createProject( + { + endpoint: env.appwriteAdmin.endpoint, + projectId: env.appwriteAdmin.projectId, + apiKey: env.appwriteAdmin.apiKey, + }, + input.name + ); + projectId = created.projectId; + } + + if (!projectId) { + throw new Error('projectId is required when createProject is false'); + } + + const now = new Date().toISOString(); + const project: AppwriteProject = { + id: crypto.randomUUID(), + name: input.name, + endpoint: input.endpoint, + projectId, + apiKey: input.apiKey, + createdAt: now, + updatedAt: now, + }; + + tenant.appwriteProjects.push(project); + tenant.updatedAt = now; + + await saveTenants(tenants); + + return project; +}; + +export const listTenantProjects = async (tenantId: string) => { + const tenants = await loadTenants(); + const tenant = tenants.find((item) => item.id === tenantId); + + if (!tenant) { + throw new Error('Tenant not found'); + } + + return tenant.appwriteProjects.map(redactProject); +}; + +export const updateTenantProject = async ( + tenantId: string, + projectRef: string, + updates: Partial +): Promise => { + const tenants = await loadTenants(); + const tenant = tenants.find((item) => item.id === tenantId); + + if (!tenant) { + throw new Error('Tenant not found'); + } + + const project = tenant.appwriteProjects.find((item) => item.id === projectRef || item.projectId === projectRef); + + if (!project) { + throw new Error('Project not found'); + } + + project.name = updates.name ?? project.name; + project.endpoint = updates.endpoint ?? project.endpoint; + project.projectId = updates.projectId ?? project.projectId; + project.apiKey = updates.apiKey ?? project.apiKey; + project.updatedAt = new Date().toISOString(); + + tenant.updatedAt = project.updatedAt; + await saveTenants(tenants); + + return project; +}; + +export const removeTenantProject = async (tenantId: string, projectRef: string): Promise => { + const tenants = await loadTenants(); + const tenant = tenants.find((item) => item.id === tenantId); + + if (!tenant) { + throw new Error('Tenant not found'); + } + + const nextProjects = tenant.appwriteProjects.filter( + (item) => item.id !== projectRef && item.projectId !== projectRef + ); + + if (nextProjects.length === tenant.appwriteProjects.length) { + throw new Error('Project not found'); + } + + tenant.appwriteProjects = nextProjects; + tenant.updatedAt = new Date().toISOString(); + await saveTenants(tenants); +}; diff --git a/backend/src/modules/tenants/tenants.store.ts b/backend/src/modules/tenants/tenants.store.ts new file mode 100644 index 0000000..4d0333b --- /dev/null +++ b/backend/src/modules/tenants/tenants.store.ts @@ -0,0 +1,27 @@ +import { promises as fs } from 'fs'; +import path from 'path'; +import { env } from '../../lib/env.js'; +import type { Tenant } from './tenants.types.js'; + +const dataFile = path.join(env.dataDir, 'tenants.json'); + +const ensureDataDir = async () => { + await fs.mkdir(env.dataDir, { recursive: true }); +}; + +export const loadTenants = async (): Promise => { + try { + const content = await fs.readFile(dataFile, 'utf-8'); + return JSON.parse(content) as Tenant[]; + } catch (error) { + if (typeof error === 'object' && error !== null && 'code' in error && (error as { code?: string }).code === 'ENOENT') { + return []; + } + throw error; + } +}; + +export const saveTenants = async (tenants: Tenant[]): Promise => { + await ensureDataDir(); + await fs.writeFile(dataFile, JSON.stringify(tenants, null, 2)); +}; diff --git a/backend/src/modules/tenants/tenants.types.ts b/backend/src/modules/tenants/tenants.types.ts new file mode 100644 index 0000000..55541c5 --- /dev/null +++ b/backend/src/modules/tenants/tenants.types.ts @@ -0,0 +1,35 @@ +import type { FinopsSummary } from '../finops/finops.service.js'; + +export type AppwriteProject = { + id: string; + name: string; + endpoint: string; + projectId: string; + apiKey: string; + createdAt: string; + updatedAt: string; +}; + +export type Tenant = { + id: string; + name: string; + slug: string; + status: 'active' | 'inactive'; + createdAt: string; + updatedAt: string; + appwriteProjects: AppwriteProject[]; + finops: FinopsSummary; +}; + +export type TenantInput = { + name: string; + slug?: string; +}; + +export type AppwriteProjectInput = { + name: string; + endpoint: string; + projectId?: string; + apiKey: string; + createProject?: boolean; +}; diff --git a/backend/src/scripts/setup-appwrite.ts b/backend/src/scripts/setup-appwrite.ts new file mode 100644 index 0000000..7b2ca09 --- /dev/null +++ b/backend/src/scripts/setup-appwrite.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +import { applySchema } from '../lib/appwrite.js'; +import { env } from '../lib/env.js'; +import { logger } from '../lib/logger.js'; + +const endpoint = process.env.APPWRITE_SETUP_ENDPOINT || env.appwriteAdmin.endpoint || env.defaults.appwriteEndpoint; +const projectId = process.env.APPWRITE_SETUP_PROJECT_ID || ''; +const apiKey = process.env.APPWRITE_SETUP_API_KEY || env.appwriteAdmin.apiKey; + +if (!endpoint || !projectId || !apiKey) { + logger.error('Missing Appwrite setup credentials', { + endpoint: Boolean(endpoint), + projectId: Boolean(projectId), + apiKey: Boolean(apiKey), + }); + process.exit(1); +} + +const run = async () => { + const result = await applySchema({ endpoint, projectId, apiKey }, { logPrefix: 'setup' }); + logger.info('Setup completed', result); +}; + +run().catch((error: Error) => { + logger.error('Setup failed', { message: error.message }); + process.exit(1); +}); diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..d9fba83 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src/**/*.ts"] +} diff --git a/docs/BACKEND.md b/docs/BACKEND.md deleted file mode 100644 index 636ebf5..0000000 --- a/docs/BACKEND.md +++ /dev/null @@ -1,43 +0,0 @@ -# Backend de leitura do Appwrite - -Este backend expõe uma API HTTP simples para consultar os documentos das coleções -criadas pelo `setup-appwrite.js` no Appwrite. - -## ✨ O que ele faz - -- Conecta no Appwrite usando as variáveis `APPWRITE_*` do `.env`. -- Exponde endpoints REST para ler dados das collections: - - `/servers` - - `/github-repos` - - `/audit-logs` (ordenado por `timestamp` desc) - - `/cloud-accounts` -- Suporta paginação via query params: - - `limit` (1–100) - - `offset` (>= 0) - -## ▶️ Como rodar - -1. Garanta que o `.env` tem as variáveis necessárias. -2. Instale as dependências do projeto (`npm install`). -3. Inicie o backend: - -```bash -npm run dev:backend -``` - -O servidor inicia em `http://localhost:4000`. - -## ⚙️ Variáveis necessárias - -- `APPWRITE_ENDPOINT` -- `APPWRITE_PROJECT_ID` -- `APPWRITE_API_KEY` -- `APPWRITE_DATABASE_ID` (ou `VITE_APPWRITE_DATABASE_ID`) -- `APPWRITE_COLLECTION_*_ID` (ou `VITE_APPWRITE_COLLECTION_*_ID`) - -## 🔍 Exemplo de requisições - -```bash -curl http://localhost:4000/servers?limit=10 -curl http://localhost:4000/audit-logs?limit=20&offset=0 -``` diff --git a/docs/SETUP_GUIDE.md b/docs/SETUP_GUIDE.md deleted file mode 100644 index fd55368..0000000 --- a/docs/SETUP_GUIDE.md +++ /dev/null @@ -1,172 +0,0 @@ -# Guia Rápido - Setup Automatizado Appwrite - -## 🎯 O Que o Script Faz - -O script `setup-appwrite.js` automatiza todo o setup do Appwrite: - -✅ Cria o Database "DevOpsPlatform" -✅ Cria 4 Collections com schemas corretos: - - **servers**: name, ip, status (enum: online/offline), region - - **github_repos**: repo_name, url, last_commit, status - - **audit_logs**: event, user_id, timestamp - - **cloud_accounts**: provider, apiKey, label - -✅ Popula com dados de exemplo: - - 4 servidores - - 3 repositórios GitHub - - 3 audit logs - - 2 cloud accounts - -✅ Atualiza o arquivo `.env` automaticamente com todos os IDs gerados - -## 📋 Passo 1: Obter API Key do Appwrite - -1. Acesse https://cloud.appwrite.io -2. Entre no seu projeto (ID: `68be03580005c05fb11f`) -3. Vá em **Settings** → **API Keys** -4. Clique em **Create API Key** -5. Dê um nome: "Setup Script" ou "Admin Key" -6. **Importante**: Marque **TODOS** os scopes (permissões) -7. Clique em **Create** -8. **Copie a API Key** (ela só aparece uma vez!) - -## 📝 Passo 2: Adicionar API Key no .env - -Edite o arquivo `.env` e preencha a linha 28: - -```env -APPWRITE_API_KEY=sua_api_key_aqui -``` - -**Exemplo**: -```env -APPWRITE_API_KEY=standard_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6 -``` - -## 🚀 Passo 3: Executar o Script - -```bash -npm run setup:appwrite -``` - -Ou diretamente: - -```bash -node setup-appwrite.js -``` - -## ✅ O Que Você Verá - -``` -🚀 Iniciando setup do Appwrite... - -📍 Endpoint: https://nyc.cloud.appwrite.io/v1 -📁 Project ID: 68be03580005c05fb11f - -📦 Criando Database "DevOpsPlatform"... -✅ Database criado: 67a1b2c3d4e5f6 - -📋 Criando collection "servers"... -✅ Collection "servers" criada: servers - -📋 Criando collection "github_repos"... -✅ Collection "github_repos" criada: github_repos - -📋 Criando collection "audit_logs"... -✅ Collection "audit_logs" criada: audit_logs - -📋 Criando collection "cloud_accounts"... -✅ Collection "cloud_accounts" criada: cloud_accounts - -🌱 Populando com dados de exemplo... - - ✅ Servidor criado: web-01 - ✅ Servidor criado: web-02 - ✅ Servidor criado: db-01 - ✅ Servidor criado: cache-01 - ✅ Repositório criado: core-platform - ✅ Repositório criado: api-backend - ✅ Repositório criado: mobile-app - ✅ Log criado: User login - ✅ Log criado: Server deployed - ✅ Log criado: Configuration updated - ✅ Conta criada: Cloudflare - ✅ Conta criada: AWS - -📝 Atualizando arquivo .env... -✅ Arquivo .env atualizado! - -🎉 Setup concluído com sucesso! - -📋 Resumo: - Database ID: 67a1b2c3d4e5f6 - servers: servers - github_repos: github_repos - audit_logs: audit_logs - cloud_accounts: cloud_accounts - -✅ Arquivo .env atualizado com os IDs - -🚀 Próximo passo: npm run dev:web -``` - -## 🔍 Verificar no Appwrite Console - -Após executar, acesse https://cloud.appwrite.io e verifique: - -1. **Databases**: Deve aparecer "DevOpsPlatform" -2. **Collections**: Dentro do database, 4 collections com dados -3. **Documents**: Cada collection terá documentos de exemplo - -## 🧪 Testar o Dashboard - -```bash -npm run dev:web -``` - -Acesse http://localhost:5173 e faça login. O dashboard deve mostrar: -- Servidores no widget Overview -- Repositórios GitHub -- Audit logs em tempo real no terminal - -## ❌ Troubleshooting - -### Erro: "APPWRITE_API_KEY is missing" - -**Solução**: Preencha a `APPWRITE_API_KEY` no arquivo `.env` (linha 28) - -### Erro: "Invalid API Key" - -**Solução**: -1. Verifique se copiou a chave completa -2. Confirme que a chave tem todos os scopes marcados -3. Tente criar uma nova API Key - -### Erro: "Collection already exists" (409) - -**Solução**: Isso é normal! O script detecta e usa as collections existentes. - -### Erro: "Permission denied" - -**Solução**: A API Key precisa ter scopes de Admin. Recrie com todos os scopes marcados. - -## 🔄 Executar Novamente - -Você pode executar o script múltiplas vezes sem problemas: -- Se database existe, ele usa o existente -- Se collections existem, elas são reutilizadas -- Dados duplicados são ignorados - -## 📝 Próximos Passos - -Após o setup bem-sucedido: - -1. ✅ Verificar `.env` foi atualizado com os IDs -2. ✅ Executar `npm run dev:web` -3. ✅ Fazer login no dashboard -4. ✅ Verificar se os dados aparecem -5. ✅ Testar o terminal de realtime (audit logs) - ---- - -**Dúvidas?** Consulte o [README principal](README.md) para mais detalhes. diff --git a/docs/appwrite-databases-schema.md b/docs/appwrite-databases-schema.md deleted file mode 100644 index 672a093..0000000 --- a/docs/appwrite-databases-schema.md +++ /dev/null @@ -1,91 +0,0 @@ -# Appwrite Database Collections - -Definição textual das coleções necessárias no Appwrite Console e um exemplo simples de script JavaScript para criá-las via SDK. - -## Collections - -### cloud_accounts -- **provider**: string (`'github'` | `'cloudflare'`), obrigatório. -- **apiKey**: string (armazenada com criptografia no Appwrite), obrigatório. -- **label**: string (nome amigável para exibir), obrigatório. - -### projects -- **name**: string (nome do projeto), obrigatório. -- **repoUrl**: string (URL do repositório), obrigatório. -- **deployStatus**: string (status do deploy), obrigatório. - -### audit_logs -- **action**: string (ação executada), obrigatório. -- **timestamp**: datetime (instante do evento), obrigatório. -- **userId**: string (ID do usuário responsável), obrigatório. - -## Script de exemplo (Node.js) - -O script abaixo usa o SDK do Appwrite para criar as coleções e seus atributos em um database existente. Ajuste as variáveis `APPWRITE_ENDPOINT`, `APPWRITE_PROJECT`, `APPWRITE_API_KEY` e `DATABASE_ID` antes de executar. - -```bash -npm install appwrite -node create-collections.js -``` - -```js -// create-collections.js -import { Client, Databases, ID } from 'appwrite'; - -const client = new Client() - .setEndpoint(process.env.APPWRITE_ENDPOINT || 'https://cloud.appwrite.io/v1') - .setProject(process.env.APPWRITE_PROJECT) - .setKey(process.env.APPWRITE_API_KEY); - -const DATABASE_ID = process.env.DATABASE_ID; // ID do banco existente -const databases = new Databases(client); - -async function createCollection({ id, name }) { - await databases.createCollection(DATABASE_ID, id, name, [ - { - type: 'document', - roles: [ - { role: 'all', permission: 'read' }, - { role: 'users', permission: 'create' }, - { role: 'users', permission: 'update' }, - { role: 'users', permission: 'delete' }, - ], - }, - ]); -} - -async function createStringAttribute(collectionId, key, size = 255, required = true, defaultValue = undefined) { - await databases.createStringAttribute(DATABASE_ID, collectionId, key, size, required, defaultValue); -} - -async function createDatetimeAttribute(collectionId, key, required = true) { - await databases.createDatetimeAttribute(DATABASE_ID, collectionId, key, required); -} - -async function main() { - // cloud_accounts - await createCollection({ id: 'cloud_accounts', name: 'Cloud Accounts' }); - await createStringAttribute('cloud_accounts', 'provider', 20); - await createStringAttribute('cloud_accounts', 'apiKey', 512); - await createStringAttribute('cloud_accounts', 'label', 100); - - // projects - await createCollection({ id: 'projects', name: 'Projects' }); - await createStringAttribute('projects', 'name', 200); - await createStringAttribute('projects', 'repoUrl', 500); - await createStringAttribute('projects', 'deployStatus', 100); - - // audit_logs - await createCollection({ id: 'audit_logs', name: 'Audit Logs' }); - await createStringAttribute('audit_logs', 'action', 200); - await createDatetimeAttribute('audit_logs', 'timestamp'); - await createStringAttribute('audit_logs', 'userId', 128); - - console.log('Collections e atributos criados.'); -} - -main().catch((err) => { - console.error('Erro ao criar collections:', err); - process.exit(1); -}); -``` diff --git a/package-lock.json b/package-lock.json index 31b95d9..2c9dc82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,29 +7,10 @@ "": { "name": "core", "version": "1.0.0", - "dependencies": { - "cors": "^2.8.5", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "node-appwrite": "^14.1.0" - }, "devDependencies": { "npm-run-all": "^4.1.5" } }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", @@ -60,12 +41,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", @@ -121,30 +96,6 @@ "dev": true, "license": "MIT" }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "license": "MIT", - "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.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -156,15 +107,6 @@ "concat-map": "0.0.1" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -188,6 +130,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -201,6 +144,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -252,55 +196,6 @@ "dev": true, "license": "MIT" }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/cross-spawn": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", @@ -372,15 +267,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -417,41 +303,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -462,21 +318,6 @@ "node": ">= 0.4" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -560,6 +401,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -569,6 +411,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -578,6 +421,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -620,12 +464,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -636,79 +474,6 @@ "node": ">=0.8.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "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.14.0", - "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" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -725,28 +490,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -797,6 +545,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -821,6 +570,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -869,6 +619,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -940,6 +691,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -968,6 +720,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -983,44 +736,6 @@ "dev": true, "license": "ISC" }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -1036,15 +751,6 @@ "node": ">= 0.4" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -1452,20 +1158,12 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", @@ -1475,57 +1173,6 @@ "node": ">= 0.10.0" } }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1539,21 +1186,6 @@ "node": "*" } }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -1561,21 +1193,6 @@ "dev": true, "license": "MIT" }, - "node_modules/node-appwrite": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-14.2.0.tgz", - "integrity": "sha512-sPPA+JzdBJRS+lM6azX85y3/6iyKQYlHcXCbjMuWLROh6IiU9EfXRW3XSUTa5HDoBrlo8ve+AnVA6BIjQfUs1g==", - "license": "BSD-3-Clause", - "dependencies": { - "node-fetch-native-with-agent": "1.7.2" - } - }, - "node_modules/node-fetch-native-with-agent": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-fetch-native-with-agent/-/node-fetch-native-with-agent-1.7.2.tgz", - "integrity": "sha512-5MaOOCuJEvcckoz7/tjdx1M6OusOY6Xc5f459IaruGStWnKzlI1qpNgaAwmn4LmFYcsSlj+jBMk84wmmRxfk5g==", - "license": "MIT" - }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -1615,19 +1232,11 @@ "node": ">= 4" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -1667,18 +1276,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -1711,15 +1308,6 @@ "node": ">=4" } }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -1737,12 +1325,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, "node_modules/path-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", @@ -1789,58 +1371,6 @@ "node": ">= 0.4" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -1941,26 +1471,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -1996,12 +1506,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, "node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -2012,51 +1516,6 @@ "semver": "bin/semver" } }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -2106,12 +1565,6 @@ "node": ">= 0.4" } }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, "node_modules/shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -2152,6 +1605,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2171,6 +1625,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2187,6 +1642,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -2205,6 +1661,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -2256,15 +1713,6 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -2393,28 +1841,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -2512,24 +1938,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -2541,15 +1949,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/package.json b/package.json index a8adecd..52ff4e5 100644 --- a/package.json +++ b/package.json @@ -4,20 +4,14 @@ "version": "1.0.0", "type": "module", "scripts": { - "dev:backend": "node backend/server.js", + "dev:backend": "npm --prefix backend run dev", "dev:dashboard": "cd dashboard && npm run dev", "dev:landing": "cd landing && deno task start", "dev:web": "npm-run-all -p dev:dashboard dev:landing", "lint:dashboard": "cd dashboard && npm run lint", - "setup:appwrite": "node setup-appwrite.js" + "setup:appwrite": "npm --prefix backend run setup:appwrite" }, "devDependencies": { "npm-run-all": "^4.1.5" - }, - "dependencies": { - "cors": "^2.8.5", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "node-appwrite": "^14.1.0" } } diff --git a/setup-appwrite.js b/setup-appwrite.js deleted file mode 100644 index 4a512b5..0000000 --- a/setup-appwrite.js +++ /dev/null @@ -1,406 +0,0 @@ -#!/usr/bin/env node - -/** - * Appwrite Setup Script - * - * Automaticamente: - * 1. Cria Database "DevOpsPlatform" - * 2. Cria 4 Collections com schemas corretos - * 3. Popula com dados de exemplo - * 4. Atualiza .env com os IDs gerados - * - * Uso: node setup-appwrite.js - */ - -import { Client, Databases, ID, Permission, Role } from 'node-appwrite'; -import * as dotenv from 'dotenv'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; -import { readFileSync, writeFileSync } from 'fs'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Carregar .env -dotenv.config(); - -// Validar variáveis necessárias -const ENDPOINT = process.env.APPWRITE_ENDPOINT; -const PROJECT_ID = process.env.APPWRITE_PROJECT_ID; -const API_KEY = process.env.APPWRITE_API_KEY; - -if (!ENDPOINT || !PROJECT_ID || !API_KEY) { - console.error('❌ Erro: Variáveis de ambiente faltando!'); - console.error(''); - console.error('Por favor, preencha no arquivo .env:'); - if (!ENDPOINT) console.error(' - APPWRITE_ENDPOINT'); - if (!PROJECT_ID) console.error(' - APPWRITE_PROJECT_ID'); - if (!API_KEY) console.error(' - APPWRITE_API_KEY'); - console.error(''); - console.error('Para obter a API_KEY:'); - console.error('1. Acesse https://cloud.appwrite.io'); - console.error('2. Vá em Settings → API Keys'); - console.error('3. Crie uma API Key com todos os scopes (Admin)'); - process.exit(1); -} - -// Inicializar cliente Appwrite -const client = new Client() - .setEndpoint(ENDPOINT) - .setProject(PROJECT_ID) - .setKey(API_KEY); - -const databases = new Databases(client); - -// IDs que serão gerados -let databaseId = ''; -const collectionIds = { - servers: '', - github_repos: '', - audit_logs: '', - cloud_accounts: '' -}; - -console.log('🚀 Iniciando setup do Appwrite...\n'); -console.log(`📍 Endpoint: ${ENDPOINT}`); -console.log(`📁 Project ID: ${PROJECT_ID}\n`); - -/** - * 1. Criar Database - */ -async function createDatabase() { - try { - console.log('📦 Criando Database "DevOpsPlatform"...'); - - const database = await databases.create( - ID.unique(), - 'DevOpsPlatform', - true // enabled - ); - - databaseId = database.$id; - console.log(`✅ Database criado: ${databaseId}\n`); - - return database; - } catch (error) { - if (error.code === 409) { - console.log('⚠️ Database já existe, buscando ID...'); - const list = await databases.list(); - const existing = list.databases.find(db => db.name === 'DevOpsPlatform'); - if (existing) { - databaseId = existing.$id; - console.log(`✅ Usando database existente: ${databaseId}\n`); - return existing; - } - } - throw error; - } -} - -/** - * 2. Criar Collection: servers - */ -async function createServersCollection() { - try { - console.log('📋 Criando collection "servers"...'); - - const collection = await databases.createCollection( - databaseId, - 'servers', - 'Servers', - [ - Permission.read(Role.any()), - Permission.create(Role.users()), - Permission.update(Role.users()), - Permission.delete(Role.users()) - ] - ); - - collectionIds.servers = collection.$id; - - // Criar atributos (sem default em required) - await databases.createStringAttribute(databaseId, collectionIds.servers, 'name', 255, true); - await databases.createStringAttribute(databaseId, collectionIds.servers, 'ip', 45, true); - await databases.createEnumAttribute(databaseId, collectionIds.servers, 'status', ['online', 'offline'], true); // Sem default - await databases.createStringAttribute(databaseId, collectionIds.servers, 'region', 100, false); - - console.log(`✅ Collection "servers" criada: ${collectionIds.servers}`); - - // Aguardar atributos serem processados - await new Promise(resolve => setTimeout(resolve, 2000)); - - } catch (error) { - if (error.code === 409) { - collectionIds.servers = 'servers'; - console.log(`⚠️ Collection "servers" já existe`); - } else { - throw error; - } - } -} - -/** - * 3. Criar Collection: github_repos - */ -async function createGitHubReposCollection() { - try { - console.log('📋 Criando collection "github_repos"...'); - - const collection = await databases.createCollection( - databaseId, - 'github_repos', - 'GitHub Repositories', - [ - Permission.read(Role.any()), - Permission.create(Role.users()), - Permission.update(Role.users()), - Permission.delete(Role.users()) - ] - ); - - collectionIds.github_repos = collection.$id; - - // Criar atributos - await databases.createStringAttribute(databaseId, collectionIds.github_repos, 'repo_name', 255, true); - await databases.createUrlAttribute(databaseId, collectionIds.github_repos, 'url', true); - await databases.createStringAttribute(databaseId, collectionIds.github_repos, 'last_commit', 255, false); - await databases.createStringAttribute(databaseId, collectionIds.github_repos, 'status', 50, false); // Opcional, sem default - - console.log(`✅ Collection "github_repos" criada: ${collectionIds.github_repos}`); - - await new Promise(resolve => setTimeout(resolve, 2000)); - - } catch (error) { - if (error.code === 409) { - collectionIds.github_repos = 'github_repos'; - console.log(`⚠️ Collection "github_repos" já existe`); - } else { - throw error; - } - } -} - -/** - * 4. Criar Collection: audit_logs - */ -async function createAuditLogsCollection() { - try { - console.log('📋 Criando collection "audit_logs"...'); - - const collection = await databases.createCollection( - databaseId, - 'audit_logs', - 'Audit Logs', - [ - Permission.read(Role.any()), - Permission.create(Role.users()), - Permission.update(Role.users()), - Permission.delete(Role.users()) - ] - ); - - collectionIds.audit_logs = collection.$id; - - // Criar atributos - await databases.createStringAttribute(databaseId, collectionIds.audit_logs, 'event', 500, true); - await databases.createStringAttribute(databaseId, collectionIds.audit_logs, 'user_id', 255, true); - await databases.createDatetimeAttribute(databaseId, collectionIds.audit_logs, 'timestamp', true); - - console.log(`✅ Collection "audit_logs" criada: ${collectionIds.audit_logs}`); - - await new Promise(resolve => setTimeout(resolve, 2000)); - - } catch (error) { - if (error.code === 409) { - collectionIds.audit_logs = 'audit_logs'; - console.log(`⚠️ Collection "audit_logs" já existe`); - } else { - throw error; - } - } -} - -/** - * 5. Criar Collection: cloud_accounts - */ -async function createCloudAccountsCollection() { - try { - console.log('📋 Criando collection "cloud_accounts"...'); - - const collection = await databases.createCollection( - databaseId, - 'cloud_accounts', - 'Cloud Accounts', - [ - Permission.read(Role.any()), - Permission.create(Role.users()), - Permission.update(Role.users()), - Permission.delete(Role.users()) - ] - ); - - collectionIds.cloud_accounts = collection.$id; - - // Criar atributos - await databases.createStringAttribute(databaseId, collectionIds.cloud_accounts, 'provider', 100, true); - await databases.createStringAttribute(databaseId, collectionIds.cloud_accounts, 'apiKey', 500, true); - await databases.createStringAttribute(databaseId, collectionIds.cloud_accounts, 'label', 255, false); - - console.log(`✅ Collection "cloud_accounts" criada: ${collectionIds.cloud_accounts}`); - - await new Promise(resolve => setTimeout(resolve, 2000)); - - } catch (error) { - if (error.code === 409) { - collectionIds.cloud_accounts = 'cloud_accounts'; - console.log(`⚠️ Collection "cloud_accounts" já existe`); - } else { - throw error; - } - } -} - -/** - * 6. Popular com dados de exemplo - */ -async function seedData() { - console.log('\n🌱 Populando com dados de exemplo...\n'); - - // Servidores - const servers = [ - { name: 'web-01', ip: '192.168.1.10', status: 'online', region: 'us-east-1' }, - { name: 'web-02', ip: '192.168.1.11', status: 'online', region: 'us-east-1' }, - { name: 'db-01', ip: '192.168.1.20', status: 'online', region: 'us-west-2' }, - { name: 'cache-01', ip: '192.168.1.30', status: 'offline', region: 'eu-west-1' }, - ]; - - for (const server of servers) { - try { - await databases.createDocument(databaseId, collectionIds.servers, ID.unique(), server); - console.log(` ✅ Servidor criado: ${server.name}`); - } catch (error) { - console.log(` ⚠️ Servidor ${server.name} já existe`); - } - } - - // Repositórios GitHub - const repos = [ - { repo_name: 'core-platform', url: 'https://github.com/rede5/core', last_commit: 'docs: adiciona setup completo', status: 'active' }, - { repo_name: 'api-backend', url: 'https://github.com/rede5/api', last_commit: 'feat: add authentication', status: 'active' }, - { repo_name: 'mobile-app', url: 'https://github.com/rede5/mobile', last_commit: 'fix: crash on startup', status: 'active' }, - ]; - - for (const repo of repos) { - try { - await databases.createDocument(databaseId, collectionIds.github_repos, ID.unique(), repo); - console.log(` ✅ Repositório criado: ${repo.repo_name}`); - } catch (error) { - console.log(` ⚠️ Repositório ${repo.repo_name} já existe`); - } - } - - // Audit Logs - const logs = [ - { event: 'User login', user_id: 'admin', timestamp: new Date().toISOString() }, - { event: 'Server deployed', user_id: 'admin', timestamp: new Date().toISOString() }, - { event: 'Configuration updated', user_id: 'admin', timestamp: new Date().toISOString() }, - ]; - - for (const log of logs) { - try { - await databases.createDocument(databaseId, collectionIds.audit_logs, ID.unique(), log); - console.log(` ✅ Log criado: ${log.event}`); - } catch (error) { - console.log(` ⚠️ Log já existe`); - } - } - - // Cloud Accounts - const accounts = [ - { provider: 'Cloudflare', apiKey: 'cf_example_key_123', label: 'Production Account' }, - { provider: 'AWS', apiKey: 'aws_example_key_456', label: 'Staging Account' }, - ]; - - for (const account of accounts) { - try { - await databases.createDocument(databaseId, collectionIds.cloud_accounts, ID.unique(), account); - console.log(` ✅ Conta criada: ${account.provider}`); - } catch (error) { - console.log(` ⚠️ Conta ${account.provider} já existe`); - } - } -} - -/** - * 7. Atualizar arquivo .env - */ -function updateEnvFile() { - console.log('\n📝 Atualizando arquivo .env...'); - - const envPath = join(__dirname, '.env'); - let envContent = readFileSync(envPath, 'utf8'); - - // Atualizar IDs - envContent = envContent.replace( - /VITE_APPWRITE_PROJECT_ID=.*/, - `VITE_APPWRITE_PROJECT_ID=${PROJECT_ID}` - ); - envContent = envContent.replace( - /VITE_APPWRITE_DATABASE_ID=.*/, - `VITE_APPWRITE_DATABASE_ID=${databaseId}` - ); - envContent = envContent.replace( - /VITE_APPWRITE_COLLECTION_SERVERS_ID=.*/, - `VITE_APPWRITE_COLLECTION_SERVERS_ID=${collectionIds.servers}` - ); - envContent = envContent.replace( - /VITE_APPWRITE_COLLECTION_GITHUB_REPOS_ID=.*/, - `VITE_APPWRITE_COLLECTION_GITHUB_REPOS_ID=${collectionIds.github_repos}` - ); - envContent = envContent.replace( - /VITE_APPWRITE_COLLECTION_AUDIT_LOGS_ID=.*/, - `VITE_APPWRITE_COLLECTION_AUDIT_LOGS_ID=${collectionIds.audit_logs}` - ); - envContent = envContent.replace( - /VITE_APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID=.*/, - `VITE_APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID=${collectionIds.cloud_accounts}` - ); - - writeFileSync(envPath, envContent); - console.log('✅ Arquivo .env atualizado!\n'); -} - -/** - * Main - */ -async function main() { - try { - await createDatabase(); - await createServersCollection(); - await createGitHubReposCollection(); - await createAuditLogsCollection(); - await createCloudAccountsCollection(); - await seedData(); - updateEnvFile(); - - console.log('\n🎉 Setup concluído com sucesso!\n'); - console.log('📋 Resumo:'); - console.log(` Database ID: ${databaseId}`); - console.log(` servers: ${collectionIds.servers}`); - console.log(` github_repos: ${collectionIds.github_repos}`); - console.log(` audit_logs: ${collectionIds.audit_logs}`); - console.log(` cloud_accounts: ${collectionIds.cloud_accounts}`); - console.log('\n✅ Arquivo .env atualizado com os IDs'); - console.log('\n🚀 Próximo passo: npm run dev:web\n'); - - } catch (error) { - console.error('\n❌ Erro durante setup:'); - console.error(error.message); - if (error.response) { - console.error('Detalhes:', error.response); - } - process.exit(1); - } -} - -main();