Add multi-tenant Appwrite backend

This commit is contained in:
Tiago Yamamoto 2025-12-27 13:35:03 -03:00
parent 9274f9c8a7
commit 0cc3bb7c7a
36 changed files with 2810 additions and 1506 deletions

View file

@ -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
View file

@ -22,3 +22,6 @@ Thumbs.db
# Build outputs
dist/
build/
# Backend data
backend/data/tenants.json

127
README.md
View file

@ -19,18 +19,55 @@
## 🎯 Visão Geral
Este monorepo contém três componentes principais:
Este monorepo contém quatro componentes principais:
- **Landing Page**: Interface pública desenvolvida com Fresh (framework Deno) e Tailwind CSS
- **Dashboard**: Painel administrativo em React + TypeScript + Vite com integração Appwrite
- **Appwrite Functions**: Três funções serverless (hello-world, sync-github, check-cloudflare-status)
- **Backend (Node.js + TypeScript)**: API administrativa multi-tenant para gerenciar projetos Appwrite
**Backend**: Appwrite Cloud - BaaS (Backend as a Service) com:
**Infra principal Appwrite** (por projeto):
- Autenticação (Email/Password)
- Database com 4 coleções (servers, github_repos, audit_logs, cloud_accounts)
- Realtime subscriptions para logs ao vivo
- Functions para automação
## 🧱 Arquitetura do Backend Multi-tenant
- **Tenants** representam empresas/clientes.
- Cada tenant possui **um ou mais projetos Appwrite** com endpoint + API key próprios.
- O backend persiste o controle local em `backend/data/tenants.json`.
- Autenticação administrativa via `ADMIN_API_TOKEN`.
## 🔁 Fluxo multi-tenant (resumo)
1. `POST /tenants` cria o tenant.
2. `POST /tenants/:id/appwrite-project` registra o projeto Appwrite do tenant.
3. `POST /appwrite/setup` aplica schema base automaticamente.
4. `GET /tenants/:id/appwrite-projects` lista projetos vinculados.
### Como adicionar um novo Appwrite (exemplo rápido)
```bash
# 1) Criar tenant
curl -X POST http://localhost:4000/tenants \\
-H \"Authorization: Bearer <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
Certifique-se de ter instalado:
@ -60,59 +97,58 @@ cd core
# 2. Instale as dependências raiz
npm install
# 3. Instale as dependências do dashboard
# 3. Instale as dependências do backend
cd backend
npm install
cd ..
# 4. Instale as dependências do dashboard
cd dashboard
npm install
cd ..
# 4. Verifique se o Deno está instalado
# 5. Verifique se o Deno está instalado
deno --version
# Se não estiver, instale: curl -fsSL https://deno.land/install.sh | sh
# 5. Configure as variáveis de ambiente
cp .env.example .env
# Edite o .env com suas credenciais Appwrite (veja seção abaixo)
# 6. Configure as variáveis de ambiente
cp backend/.env.example backend/.env
# Edite o backend/.env com suas credenciais Appwrite (veja seção abaixo)
# 6. Configure o Appwrite Cloud (veja seção "Setup Appwrite Cloud")
# 7. Configure o Appwrite Cloud (veja seção "Setup Appwrite Cloud")
# 7. Execute o projeto
# 8. Execute o projeto
npm run dev:web
npm run dev:backend
```
## ⚙️ Configuração Detalhada
### Variáveis de Ambiente
O arquivo `.env` na raiz do projeto contém todas as configurações necessárias. Copie o `.env.example` e preencha os valores:
O backend usa um arquivo `.env` próprio em `backend/.env`. Copie o `.env.example` do backend e preencha os valores:
```bash
cp .env.example .env
cp backend/.env.example backend/.env
```
#### Referência Completa de Variáveis
#### Referência Completa de Variáveis (Backend)
| Variável | Onde Obter | Obrigatória | Descrição |
|----------|------------|-------------|-----------|
| **Configuração Server-Side (Scripts Node.js e Functions)** | | | |
| `APPWRITE_ENDPOINT` | Fixo | ✅ | URL da API Appwrite. Use `https://cloud.appwrite.io/v1` |
| `APPWRITE_PROJECT_ID` | Console Appwrite | ✅ | ID do projeto. Obtido em: Dashboard → Seu Projeto → Settings |
| `APPWRITE_API_KEY` | Console Appwrite | ✅ | Chave API com permissões Admin. Criar em: Settings → API Keys → Create API Key → Selecione todos os scopes |
| `APPWRITE_FUNCTIONS_ENDPOINT` | Opcional | ❌ | Endpoint customizado para Functions. Deixe vazio para usar o mesmo do `APPWRITE_ENDPOINT` |
| `APPWRITE_FUNCTIONS_API_KEY` | Opcional | ❌ | API Key separada para Functions. Deixe vazio para usar `APPWRITE_API_KEY` |
| **Configuração Client-Side (React Dashboard - Vite)** | | | |
| `VITE_APPWRITE_ENDPOINT` | Fixo | ✅ | Mesmo que `APPWRITE_ENDPOINT`. Prefixo `VITE_` expõe no browser |
| `VITE_APPWRITE_PROJECT_ID` | Console Appwrite | ✅ | Mesmo que `APPWRITE_PROJECT_ID` |
| `VITE_APPWRITE_DATABASE_ID` | Console Appwrite | ✅ | ID do Database criado. Obtido em: Databases → Seu Database → Settings |
| `VITE_APPWRITE_COLLECTION_SERVERS_ID` | Console Appwrite | ✅ | ID da coleção `servers`. Obtido em: Databases → Collections → servers → Settings |
| `VITE_APPWRITE_COLLECTION_GITHUB_REPOS_ID` | Console Appwrite | ✅ | ID da coleção `github_repos` |
| `VITE_APPWRITE_COLLECTION_AUDIT_LOGS_ID` | Console Appwrite | ✅ | ID da coleção `audit_logs` (usado para Realtime no terminal) |
| `VITE_APPWRITE_COLLECTION_CLOUDFLARE_ACCOUNTS_ID` | Console Appwrite | ✅ | ID da coleção `cloud_accounts` ou `cloudflare_accounts` |
| Variável | Obrigatória | Descrição |
|----------|-------------|-----------|
| `ADMIN_API_TOKEN` | ✅ | Token de acesso administrativo (`Authorization: Bearer ...`) |
| `APPWRITE_ADMIN_ENDPOINT` | ✅ | Endpoint Appwrite para criar projetos |
| `APPWRITE_ADMIN_PROJECT_ID` | ✅ | Project ID administrativo (normalmente `console`) |
| `APPWRITE_ADMIN_API_KEY` | ✅ | API Key com permissões admin para projetos |
| `DEFAULT_APPWRITE_ENDPOINT` | ✅ | Endpoint padrão para novos tenants |
| `APPWRITE_DEFAULT_RUNTIME` | ❌ | Runtime padrão das Functions (ex: `deno-1.35`) |
| `DATA_DIR` | ❌ | Diretório local para persistir tenants (default: `./data`) |
**⚠️ IMPORTANTE**: Variáveis com prefixo `VITE_` são expostas no JavaScript do browser. **NUNCA** coloque informações sensíveis (API Keys) nelas!
> As variáveis client-side do Dashboard permanecem em seu próprio `.env` (prefixo `VITE_`). **Nunca** use API keys no front-end.
### Setup Appwrite Cloud
Siga este passo a passo para configurar o backend:
Siga este passo a passo para configurar o Appwrite usado pelo dashboard. O backend multi-tenant aplica o próprio schema via `POST /appwrite/setup` quando necessário.
#### 1. Criar Projeto
@ -279,6 +315,13 @@ npm run dev:dashboard
```
Acesse: http://localhost:5173
**Backend (Node.js + TypeScript):**
```bash
npm run dev:backend
# Ou: cd backend && npm run dev
```
Acesse: http://localhost:4000
Login no dashboard usa as credenciais criadas no Appwrite (ex: `admin@test.com` / `admin123`).
### Build para Produção
@ -300,13 +343,21 @@ deno task build
```
core/
├── .env # Variáveis de ambiente (NÃO commitar!)
├── .env.example # Template de variáveis
├── package.json # Scripts raiz e npm-run-all
├── README.md # 📄 Este arquivo
├── SECURITY.md # Política de segurança
├── appwrite.json # Configuração Appwrite CLI
├── appwrite-databases-schema.md # Schema detalhado do banco
├── backend/ # 🧠 API administrativa (Node + TS)
│ ├── src/
│ │ ├── modules/ # Tenants, projects, auth, finops
│ │ ├── lib/ # Appwrite SDK, env, logger
│ │ ├── scripts/ # Setup Appwrite
│ │ ├── config/ # appwrite.json
│ │ ├── docs/ # Docs backend
│ │ └── main.ts # Entry point
│ ├── Dockerfile
│ ├── docker-compose.yml
│ ├── package.json
│ ├── tsconfig.json
│ └── .env.example
├── dashboard/ # 🎨 Painel React + Vite
│ ├── src/
@ -342,10 +393,12 @@ core/
| Script | Comando | Descrição |
|--------|---------|-----------|
| `dev:backend` | `npm run dev:backend` | Inicia a API backend multi-tenant |
| `dev:dashboard` | `npm run dev:dashboard` | Inicia somente o dashboard |
| `dev:landing` | `npm run dev:landing` | Inicia somente a landing |
| `dev:web` | `npm run dev:web` | Inicia dashboard + landing em paralelo |
| `lint:dashboard` | `npm run lint:dashboard` | Executa ESLint no dashboard |
| `setup:appwrite` | `npm run setup:appwrite` | Aplica o schema base Appwrite |
### Dashboard (`cd dashboard`)
@ -397,7 +450,7 @@ deno task check
# Deve passar formatting, linting e type-check
# 6. Verificar arquivo .env
cat ../.env
cat backend/.env
# Deve ter todos os IDs preenchidos (não vazios)
```
@ -539,7 +592,7 @@ Ou manualmente via Appwrite Console → Functions.
- **NUNCA** commite o arquivo `.env`
- API Keys devem ter scopes mínimos necessários em produção
- Habilite MFA no Appwrite Console
- Revise `SECURITY.md` para reportar vulnerabilidades
- Revise `backend/src/docs/SECURITY.md` para reportar vulnerabilidades
## 📝 Licença

18
backend/.env.example Normal file
View 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
View 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
View file

View 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

File diff suppressed because it is too large Load diff

26
backend/package.json Normal file
View 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"
}
}

View file

@ -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}`);
});

View file

@ -1,11 +1,11 @@
{
"provider": "appwrite-cloud",
"description": "Appwrite Cloud configuration for DevOps orchestration platform",
"endpoint": "${APPWRITE_ENDPOINT}",
"projectId": "${APPWRITE_PROJECT_ID}",
"apiKey": "${APPWRITE_API_KEY}",
"endpoint": "${APPWRITE_ADMIN_ENDPOINT}",
"projectId": "${APPWRITE_ADMIN_PROJECT_ID}",
"apiKey": "${APPWRITE_ADMIN_API_KEY}",
"functions": {
"defaultRuntime": "deno-1.35",
"source": "./appwrite-functions"
"source": "../../appwrite-functions"
}
}

View 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`

View 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).

View 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
View 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
View 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
View 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
View 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}`);
});

View 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();
};

View 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(),
});

View 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);
}
});

View 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;
};

View 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')],
},
],
};

View 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);
}
});

View 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);
};

View 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));
};

View 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;
};

View 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
View 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"]
}

View file

@ -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` (1100)
- `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
```

View file

@ -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.

View file

@ -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
View file

@ -7,29 +7,10 @@
"": {
"name": "core",
"version": "1.0.0",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"node-appwrite": "^14.1.0"
},
"devDependencies": {
"npm-run-all": "^4.1.5"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
@ -60,12 +41,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
"node_modules/arraybuffer.prototype.slice": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
@ -121,30 +96,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "~1.2.0",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.14.0",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@ -156,15 +107,6 @@
"concat-map": "0.0.1"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@ -188,6 +130,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -201,6 +144,7 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@ -252,55 +196,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/cross-spawn": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
@ -372,15 +267,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
@ -417,41 +303,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
"license": "MIT",
"engines": {
"node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16"
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@ -462,21 +318,6 @@
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/error-ex": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
@ -560,6 +401,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -569,6 +411,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -578,6 +421,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@ -620,12 +464,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@ -636,79 +474,6 @@
"node": ">=0.8.0"
}
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.3",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
"cookie-signature": "~1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.3.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.14.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
"serve-static": "~1.16.2",
"setprototypeof": "1.2.0",
"statuses": "~2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "~2.4.1",
"parseurl": "~1.3.3",
"statuses": "~2.0.2",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@ -725,28 +490,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -797,6 +545,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@ -821,6 +570,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@ -869,6 +619,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -940,6 +691,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -968,6 +720,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@ -983,44 +736,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@ -1036,15 +751,6 @@
"node": ">= 0.4"
}
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@ -1452,20 +1158,12 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/memorystream": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
@ -1475,57 +1173,6 @@
"node": ">= 0.10.0"
}
},
"node_modules/merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -1539,21 +1186,6 @@
"node": "*"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@ -1561,21 +1193,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-appwrite": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-14.2.0.tgz",
"integrity": "sha512-sPPA+JzdBJRS+lM6azX85y3/6iyKQYlHcXCbjMuWLROh6IiU9EfXRW3XSUTa5HDoBrlo8ve+AnVA6BIjQfUs1g==",
"license": "BSD-3-Clause",
"dependencies": {
"node-fetch-native-with-agent": "1.7.2"
}
},
"node_modules/node-fetch-native-with-agent": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/node-fetch-native-with-agent/-/node-fetch-native-with-agent-1.7.2.tgz",
"integrity": "sha512-5MaOOCuJEvcckoz7/tjdx1M6OusOY6Xc5f459IaruGStWnKzlI1qpNgaAwmn4LmFYcsSlj+jBMk84wmmRxfk5g==",
"license": "MIT"
},
"node_modules/normalize-package-data": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
@ -1615,19 +1232,11 @@
"node": ">= 4"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -1667,18 +1276,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/own-keys": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@ -1711,15 +1308,6 @@
"node": ">=4"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-key": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
@ -1737,12 +1325,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/path-type": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
@ -1789,58 +1371,6 @@
"node": ">= 0.4"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/read-pkg": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
@ -1941,26 +1471,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safe-push-apply": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
@ -1996,12 +1506,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
@ -2012,51 +1516,6 @@
"semver": "bin/semver"
}
},
"node_modules/send": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "~0.5.2",
"http-errors": "~2.0.1",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "~2.4.1",
"range-parser": "~1.2.1",
"statuses": "~2.0.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/serve-static": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
"license": "MIT",
"dependencies": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "~0.19.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@ -2106,12 +1565,6 @@
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@ -2152,6 +1605,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -2171,6 +1625,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -2187,6 +1642,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
@ -2205,6 +1661,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
@ -2256,15 +1713,6 @@
"dev": true,
"license": "CC0-1.0"
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@ -2393,28 +1841,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/typed-array-buffer": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
@ -2512,24 +1938,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"license": "MIT",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@ -2541,15 +1949,6 @@
"spdx-expression-parse": "^3.0.0"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",

View file

@ -4,20 +4,14 @@
"version": "1.0.0",
"type": "module",
"scripts": {
"dev:backend": "node backend/server.js",
"dev:backend": "npm --prefix backend run dev",
"dev:dashboard": "cd dashboard && npm run dev",
"dev:landing": "cd landing && deno task start",
"dev:web": "npm-run-all -p dev:dashboard dev:landing",
"lint:dashboard": "cd dashboard && npm run lint",
"setup:appwrite": "node setup-appwrite.js"
"setup:appwrite": "npm --prefix backend run setup:appwrite"
},
"devDependencies": {
"npm-run-all": "^4.1.5"
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"node-appwrite": "^14.1.0"
}
}

View file

@ -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();