Add multi-tenant Appwrite backend
This commit is contained in:
parent
9274f9c8a7
commit
0cc3bb7c7a
36 changed files with 2810 additions and 1506 deletions
18
.env.example
18
.env.example
|
|
@ -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=
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -22,3 +22,6 @@ Thumbs.db
|
||||||
# Build outputs
|
# Build outputs
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
|
# Backend data
|
||||||
|
backend/data/tenants.json
|
||||||
|
|
|
||||||
127
README.md
127
README.md
|
|
@ -19,18 +19,55 @@
|
||||||
|
|
||||||
## 🎯 Visão Geral
|
## 🎯 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
|
- **Landing Page**: Interface pública desenvolvida com Fresh (framework Deno) e Tailwind CSS
|
||||||
- **Dashboard**: Painel administrativo em React + TypeScript + Vite com integração Appwrite
|
- **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)
|
- **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)
|
- Autenticação (Email/Password)
|
||||||
- Database com 4 coleções (servers, github_repos, audit_logs, cloud_accounts)
|
- Database com 4 coleções (servers, github_repos, audit_logs, cloud_accounts)
|
||||||
- Realtime subscriptions para logs ao vivo
|
- Realtime subscriptions para logs ao vivo
|
||||||
- Functions para automação
|
- 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 <ADMIN_API_TOKEN>\" \\
|
||||||
|
-H \"Content-Type: application/json\" \\
|
||||||
|
-d '{\"name\":\"Acme Corp\"}'
|
||||||
|
|
||||||
|
# 2) Registrar projeto Appwrite do tenant
|
||||||
|
curl -X POST http://localhost:4000/tenants/<TENANT_ID>/appwrite-project \\
|
||||||
|
-H \"Authorization: Bearer <ADMIN_API_TOKEN>\" \\
|
||||||
|
-H \"Content-Type: application/json\" \\
|
||||||
|
-d '{\"name\":\"Acme Project\",\"endpoint\":\"https://cloud.appwrite.io/v1\",\"projectId\":\"<PROJECT_ID>\",\"apiKey\":\"<API_KEY>\"}'
|
||||||
|
|
||||||
|
# 3) Aplicar schema base
|
||||||
|
curl -X POST http://localhost:4000/appwrite/setup \\
|
||||||
|
-H \"Authorization: Bearer <ADMIN_API_TOKEN>\" \\
|
||||||
|
-H \"Content-Type: application/json\" \\
|
||||||
|
-d '{\"tenantId\":\"<TENANT_ID>\",\"projectRef\":\"<PROJECT_ID>\"}'
|
||||||
|
```
|
||||||
|
|
||||||
## 🛠 Pré-requisitos
|
## 🛠 Pré-requisitos
|
||||||
|
|
||||||
Certifique-se de ter instalado:
|
Certifique-se de ter instalado:
|
||||||
|
|
@ -60,59 +97,58 @@ cd core
|
||||||
# 2. Instale as dependências raiz
|
# 2. Instale as dependências raiz
|
||||||
npm install
|
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
|
cd dashboard
|
||||||
npm install
|
npm install
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
# 4. Verifique se o Deno está instalado
|
# 5. Verifique se o Deno está instalado
|
||||||
deno --version
|
deno --version
|
||||||
# Se não estiver, instale: curl -fsSL https://deno.land/install.sh | sh
|
# Se não estiver, instale: curl -fsSL https://deno.land/install.sh | sh
|
||||||
|
|
||||||
# 5. Configure as variáveis de ambiente
|
# 6. Configure as variáveis de ambiente
|
||||||
cp .env.example .env
|
cp backend/.env.example backend/.env
|
||||||
# Edite o .env com suas credenciais Appwrite (veja seção abaixo)
|
# 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:web
|
||||||
|
npm run dev:backend
|
||||||
```
|
```
|
||||||
|
|
||||||
## ⚙️ Configuração Detalhada
|
## ⚙️ Configuração Detalhada
|
||||||
|
|
||||||
### Variáveis de Ambiente
|
### 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
|
```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 |
|
| Variável | Obrigatória | Descrição |
|
||||||
|----------|------------|-------------|-----------|
|
|----------|-------------|-----------|
|
||||||
| **Configuração Server-Side (Scripts Node.js e Functions)** | | | |
|
| `ADMIN_API_TOKEN` | ✅ | Token de acesso administrativo (`Authorization: Bearer ...`) |
|
||||||
| `APPWRITE_ENDPOINT` | Fixo | ✅ | URL da API Appwrite. Use `https://cloud.appwrite.io/v1` |
|
| `APPWRITE_ADMIN_ENDPOINT` | ✅ | Endpoint Appwrite para criar projetos |
|
||||||
| `APPWRITE_PROJECT_ID` | Console Appwrite | ✅ | ID do projeto. Obtido em: Dashboard → Seu Projeto → Settings |
|
| `APPWRITE_ADMIN_PROJECT_ID` | ✅ | Project ID administrativo (normalmente `console`) |
|
||||||
| `APPWRITE_API_KEY` | Console Appwrite | ✅ | Chave API com permissões Admin. Criar em: Settings → API Keys → Create API Key → Selecione todos os scopes |
|
| `APPWRITE_ADMIN_API_KEY` | ✅ | API Key com permissões admin para projetos |
|
||||||
| `APPWRITE_FUNCTIONS_ENDPOINT` | Opcional | ❌ | Endpoint customizado para Functions. Deixe vazio para usar o mesmo do `APPWRITE_ENDPOINT` |
|
| `DEFAULT_APPWRITE_ENDPOINT` | ✅ | Endpoint padrão para novos tenants |
|
||||||
| `APPWRITE_FUNCTIONS_API_KEY` | Opcional | ❌ | API Key separada para Functions. Deixe vazio para usar `APPWRITE_API_KEY` |
|
| `APPWRITE_DEFAULT_RUNTIME` | ❌ | Runtime padrão das Functions (ex: `deno-1.35`) |
|
||||||
| **Configuração Client-Side (React Dashboard - Vite)** | | | |
|
| `DATA_DIR` | ❌ | Diretório local para persistir tenants (default: `./data`) |
|
||||||
| `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` |
|
|
||||||
|
|
||||||
**⚠️ 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
|
### 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
|
#### 1. Criar Projeto
|
||||||
|
|
||||||
|
|
@ -279,6 +315,13 @@ npm run dev:dashboard
|
||||||
```
|
```
|
||||||
Acesse: http://localhost:5173
|
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`).
|
Login no dashboard usa as credenciais criadas no Appwrite (ex: `admin@test.com` / `admin123`).
|
||||||
|
|
||||||
### Build para Produção
|
### Build para Produção
|
||||||
|
|
@ -300,13 +343,21 @@ deno task build
|
||||||
|
|
||||||
```
|
```
|
||||||
core/
|
core/
|
||||||
├── .env # Variáveis de ambiente (NÃO commitar!)
|
|
||||||
├── .env.example # Template de variáveis
|
|
||||||
├── package.json # Scripts raiz e npm-run-all
|
├── package.json # Scripts raiz e npm-run-all
|
||||||
├── README.md # 📄 Este arquivo
|
├── README.md # 📄 Este arquivo
|
||||||
├── SECURITY.md # Política de segurança
|
├── backend/ # 🧠 API administrativa (Node + TS)
|
||||||
├── appwrite.json # Configuração Appwrite CLI
|
│ ├── src/
|
||||||
├── appwrite-databases-schema.md # Schema detalhado do banco
|
│ │ ├── 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
|
├── dashboard/ # 🎨 Painel React + Vite
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
|
|
@ -342,10 +393,12 @@ core/
|
||||||
|
|
||||||
| Script | Comando | Descrição |
|
| 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:dashboard` | `npm run dev:dashboard` | Inicia somente o dashboard |
|
||||||
| `dev:landing` | `npm run dev:landing` | Inicia somente a landing |
|
| `dev:landing` | `npm run dev:landing` | Inicia somente a landing |
|
||||||
| `dev:web` | `npm run dev:web` | Inicia dashboard + landing em paralelo |
|
| `dev:web` | `npm run dev:web` | Inicia dashboard + landing em paralelo |
|
||||||
| `lint:dashboard` | `npm run lint:dashboard` | Executa ESLint no dashboard |
|
| `lint:dashboard` | `npm run lint:dashboard` | Executa ESLint no dashboard |
|
||||||
|
| `setup:appwrite` | `npm run setup:appwrite` | Aplica o schema base Appwrite |
|
||||||
|
|
||||||
### Dashboard (`cd dashboard`)
|
### Dashboard (`cd dashboard`)
|
||||||
|
|
||||||
|
|
@ -397,7 +450,7 @@ deno task check
|
||||||
# Deve passar formatting, linting e type-check
|
# Deve passar formatting, linting e type-check
|
||||||
|
|
||||||
# 6. Verificar arquivo .env
|
# 6. Verificar arquivo .env
|
||||||
cat ../.env
|
cat backend/.env
|
||||||
# Deve ter todos os IDs preenchidos (não vazios)
|
# Deve ter todos os IDs preenchidos (não vazios)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -539,7 +592,7 @@ Ou manualmente via Appwrite Console → Functions.
|
||||||
- **NUNCA** commite o arquivo `.env`
|
- **NUNCA** commite o arquivo `.env`
|
||||||
- API Keys devem ter scopes mínimos necessários em produção
|
- API Keys devem ter scopes mínimos necessários em produção
|
||||||
- Habilite MFA no Appwrite Console
|
- Habilite MFA no Appwrite Console
|
||||||
- Revise `SECURITY.md` para reportar vulnerabilidades
|
- Revise `backend/src/docs/SECURITY.md` para reportar vulnerabilidades
|
||||||
|
|
||||||
## 📝 Licença
|
## 📝 Licença
|
||||||
|
|
||||||
|
|
|
||||||
18
backend/.env.example
Normal file
18
backend/.env.example
Normal file
|
|
@ -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=
|
||||||
17
backend/Dockerfile
Normal file
17
backend/Dockerfile
Normal file
|
|
@ -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"]
|
||||||
0
backend/data/.gitkeep
Normal file
0
backend/data/.gitkeep
Normal file
11
backend/docker-compose.yml
Normal file
11
backend/docker-compose.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "4000:4000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
1587
backend/package-lock.json
generated
Normal file
1587
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
backend/package.json
Normal file
26
backend/package.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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}`);
|
|
||||||
});
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"provider": "appwrite-cloud",
|
"provider": "appwrite-cloud",
|
||||||
"description": "Appwrite Cloud configuration for DevOps orchestration platform",
|
"description": "Appwrite Cloud configuration for DevOps orchestration platform",
|
||||||
"endpoint": "${APPWRITE_ENDPOINT}",
|
"endpoint": "${APPWRITE_ADMIN_ENDPOINT}",
|
||||||
"projectId": "${APPWRITE_PROJECT_ID}",
|
"projectId": "${APPWRITE_ADMIN_PROJECT_ID}",
|
||||||
"apiKey": "${APPWRITE_API_KEY}",
|
"apiKey": "${APPWRITE_ADMIN_API_KEY}",
|
||||||
"functions": {
|
"functions": {
|
||||||
"defaultRuntime": "deno-1.35",
|
"defaultRuntime": "deno-1.35",
|
||||||
"source": "./appwrite-functions"
|
"source": "../../appwrite-functions"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
30
backend/src/docs/BACKEND.md
Normal file
30
backend/src/docs/BACKEND.md
Normal file
|
|
@ -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`
|
||||||
23
backend/src/docs/SETUP_GUIDE.md
Normal file
23
backend/src/docs/SETUP_GUIDE.md
Normal file
|
|
@ -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=<project_id>
|
||||||
|
APPWRITE_SETUP_API_KEY=<api_key>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Executar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm run setup:appwrite
|
||||||
|
```
|
||||||
|
|
||||||
|
O script é idempotente (pode rodar várias vezes sem duplicar recursos).
|
||||||
46
backend/src/docs/appwrite-databases-schema.md
Normal file
46
backend/src/docs/appwrite-databases-schema.md
Normal file
|
|
@ -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`
|
||||||
184
backend/src/lib/appwrite.ts
Normal file
184
backend/src/lib/appwrite.ts
Normal file
|
|
@ -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<ApplySchemaResult> => {
|
||||||
|
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 };
|
||||||
|
};
|
||||||
30
backend/src/lib/env.ts
Normal file
30
backend/src/lib/env.ts
Normal file
|
|
@ -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',
|
||||||
|
},
|
||||||
|
};
|
||||||
15
backend/src/lib/logger.ts
Normal file
15
backend/src/lib/logger.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
export type LogMeta = Record<string, unknown> | 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)}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
34
backend/src/main.ts
Normal file
34
backend/src/main.ts
Normal file
|
|
@ -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}`);
|
||||||
|
});
|
||||||
22
backend/src/modules/auth/auth.middleware.ts
Normal file
22
backend/src/modules/auth/auth.middleware.ts
Normal file
|
|
@ -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();
|
||||||
|
};
|
||||||
24
backend/src/modules/finops/finops.service.ts
Normal file
24
backend/src/modules/finops/finops.service.ts
Normal file
|
|
@ -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(),
|
||||||
|
});
|
||||||
36
backend/src/modules/projects/appwrite.routes.ts
Normal file
36
backend/src/modules/projects/appwrite.routes.ts
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
56
backend/src/modules/projects/appwrite.service.ts
Normal file
56
backend/src/modules/projects/appwrite.service.ts
Normal file
|
|
@ -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<AppwriteProject> => {
|
||||||
|
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;
|
||||||
|
};
|
||||||
179
backend/src/modules/projects/appwriteSchema.ts
Normal file
179
backend/src/modules/projects/appwriteSchema.ts
Normal file
|
|
@ -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')],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
85
backend/src/modules/tenants/tenants.routes.ts
Normal file
85
backend/src/modules/tenants/tenants.routes.ts
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
165
backend/src/modules/tenants/tenants.service.ts
Normal file
165
backend/src/modules/tenants/tenants.service.ts
Normal file
|
|
@ -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<Tenant> => {
|
||||||
|
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<AppwriteProject> => {
|
||||||
|
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<AppwriteProjectInput>
|
||||||
|
): Promise<AppwriteProject> => {
|
||||||
|
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<void> => {
|
||||||
|
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);
|
||||||
|
};
|
||||||
27
backend/src/modules/tenants/tenants.store.ts
Normal file
27
backend/src/modules/tenants/tenants.store.ts
Normal file
|
|
@ -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<Tenant[]> => {
|
||||||
|
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<void> => {
|
||||||
|
await ensureDataDir();
|
||||||
|
await fs.writeFile(dataFile, JSON.stringify(tenants, null, 2));
|
||||||
|
};
|
||||||
35
backend/src/modules/tenants/tenants.types.ts
Normal file
35
backend/src/modules/tenants/tenants.types.ts
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
28
backend/src/scripts/setup-appwrite.ts
Normal file
28
backend/src/scripts/setup-appwrite.ts
Normal file
|
|
@ -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);
|
||||||
|
});
|
||||||
15
backend/tsconfig.json
Normal file
15
backend/tsconfig.json
Normal file
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
```
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
637
package-lock.json
generated
637
package-lock.json
generated
|
|
@ -7,29 +7,10 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "core",
|
"name": "core",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"dotenv": "^16.4.5",
|
|
||||||
"express": "^4.19.2",
|
|
||||||
"node-appwrite": "^14.1.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"npm-run-all": "^4.1.5"
|
"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": {
|
"node_modules/ansi-styles": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||||
|
|
@ -60,12 +41,6 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/arraybuffer.prototype.slice": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
|
||||||
|
|
@ -121,30 +96,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
|
|
@ -156,15 +107,6 @@
|
||||||
"concat-map": "0.0.1"
|
"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": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
|
||||||
|
|
@ -188,6 +130,7 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
|
|
@ -201,6 +144,7 @@
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
|
@ -252,55 +196,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "6.0.6",
|
"version": "6.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
|
||||||
|
|
@ -372,15 +267,6 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/define-data-property": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
|
"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"
|
"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": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
|
@ -462,21 +318,6 @@
|
||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/error-ex": {
|
||||||
"version": "1.3.4",
|
"version": "1.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
|
||||||
|
|
@ -560,6 +401,7 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
|
@ -569,6 +411,7 @@
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
|
@ -578,6 +421,7 @@
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0"
|
"es-errors": "^1.3.0"
|
||||||
|
|
@ -620,12 +464,6 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/escape-string-regexp": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||||
|
|
@ -636,79 +474,6 @@
|
||||||
"node": ">=0.8.0"
|
"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": {
|
"node_modules/for-each": {
|
||||||
"version": "0.3.5",
|
"version": "0.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
|
||||||
|
|
@ -725,28 +490,11 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
|
@ -797,6 +545,7 @@
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
|
@ -821,6 +570,7 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dunder-proto": "^1.0.1",
|
"dunder-proto": "^1.0.1",
|
||||||
|
|
@ -869,6 +619,7 @@
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
|
@ -940,6 +691,7 @@
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
|
@ -968,6 +720,7 @@
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
|
|
@ -983,44 +736,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/internal-slot": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||||
|
|
@ -1036,15 +751,6 @@
|
||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
|
|
@ -1452,20 +1158,12 @@
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/memorystream": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
|
||||||
|
|
@ -1475,57 +1173,6 @@
|
||||||
"node": ">= 0.10.0"
|
"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": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
|
|
@ -1539,21 +1186,6 @@
|
||||||
"node": "*"
|
"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": {
|
"node_modules/nice-try": {
|
||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||||
|
|
@ -1561,21 +1193,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/normalize-package-data": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||||
|
|
@ -1615,19 +1232,11 @@
|
||||||
"node": ">= 4"
|
"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": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
|
|
@ -1667,18 +1276,6 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/own-keys": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||||
|
|
@ -1711,15 +1308,6 @@
|
||||||
"node": ">=4"
|
"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": {
|
"node_modules/path-key": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
|
||||||
|
|
@ -1737,12 +1325,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/path-type": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
|
||||||
|
|
@ -1789,58 +1371,6 @@
|
||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/read-pkg": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
|
||||||
|
|
@ -1941,26 +1471,6 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/safe-push-apply": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
|
"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"
|
"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": {
|
"node_modules/semver": {
|
||||||
"version": "5.7.2",
|
"version": "5.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||||
|
|
@ -2012,51 +1516,6 @@
|
||||||
"semver": "bin/semver"
|
"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": {
|
"node_modules/set-function-length": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
|
|
@ -2106,12 +1565,6 @@
|
||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/shebang-command": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||||
|
|
@ -2152,6 +1605,7 @@
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
|
|
@ -2171,6 +1625,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
|
|
@ -2187,6 +1642,7 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bound": "^1.0.2",
|
"call-bound": "^1.0.2",
|
||||||
|
|
@ -2205,6 +1661,7 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bound": "^1.0.2",
|
"call-bound": "^1.0.2",
|
||||||
|
|
@ -2256,15 +1713,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "CC0-1.0"
|
"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": {
|
"node_modules/stop-iteration-iterator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
"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"
|
"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": {
|
"node_modules/typed-array-buffer": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
"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"
|
"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": {
|
"node_modules/validate-npm-package-license": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
"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"
|
"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": {
|
"node_modules/which": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -4,20 +4,14 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev:backend": "node backend/server.js",
|
"dev:backend": "npm --prefix backend run dev",
|
||||||
"dev:dashboard": "cd dashboard && npm run dev",
|
"dev:dashboard": "cd dashboard && npm run dev",
|
||||||
"dev:landing": "cd landing && deno task start",
|
"dev:landing": "cd landing && deno task start",
|
||||||
"dev:web": "npm-run-all -p dev:dashboard dev:landing",
|
"dev:web": "npm-run-all -p dev:dashboard dev:landing",
|
||||||
"lint:dashboard": "cd dashboard && npm run lint",
|
"lint:dashboard": "cd dashboard && npm run lint",
|
||||||
"setup:appwrite": "node setup-appwrite.js"
|
"setup:appwrite": "npm --prefix backend run setup:appwrite"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"npm-run-all": "^4.1.5"
|
"npm-run-all": "^4.1.5"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"dotenv": "^16.4.5",
|
|
||||||
"express": "^4.19.2",
|
|
||||||
"node-appwrite": "^14.1.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
Loading…
Reference in a new issue