diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93f1361 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..aa9e6fc --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,30 @@ +# Segurança e Permissões do Appwrite + +As coleções do projeto seguem uma política de Row-Level Security (RLS) para proteger os dados dos usuários e os registros de auditoria. + +## Princípios gerais +- Cada documento pertence a um usuário autenticado, identificado pelo `userId` armazenado no documento. +- Operações de leitura e gravação são restritas ao proprietário do documento (role `member`). +- Logs de auditoria globais só podem ser consultados por usuários com a role `admin`. + +## Regras por coleção + +### `cloud_accounts` +- **Leitura**: apenas o usuário que criou o documento (role `member`). +- **Criação**: apenas usuários autenticados. +- **Atualização/Exclusão**: somente o proprietário do documento. + +### `projects` +- **Leitura**: restrita ao proprietário do documento. +- **Criação**: usuários autenticados podem criar seus próprios registros. +- **Atualização/Exclusão**: apenas o proprietário do documento. + +### `audit_logs` +- **Leitura**: exclusiva para usuários com role `admin` (para auditoria global). +- **Criação**: serviços e funções podem registrar ações em nome dos usuários; cada entrada mantém o `userId` responsável. +- **Atualização/Exclusão**: não permitidas para usuários finais; apenas processos administrativos podem gerenciar retenção. + +## Considerações adicionais +- Tokens e chaves sensíveis (GitHub, Cloudflare) devem ser armazenados em `cloud_accounts` e nunca enviados ao frontend. +- Funções Cloud (como as de proxy) devem validar o `userId` associado ao documento antes de usar qualquer chave. +- Revogue ou rotacione chaves comprometidas removendo ou atualizando o documento correspondente na coleção. diff --git a/appwrite-functions/check-cloudflare-status/function.json b/appwrite-functions/check-cloudflare-status/function.json new file mode 100644 index 0000000..9b52026 --- /dev/null +++ b/appwrite-functions/check-cloudflare-status/function.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://appwrite.io/docs/schemas/functions.json", + "name": "check-cloudflare-status", + "entrypoint": "src/index.js", + "runtime": "node-20.0", + "commands": ["npm install"], + "ignore": ["node_modules", ".npm", "npm-debug.log", "build"] +} diff --git a/appwrite-functions/check-cloudflare-status/package-lock.json b/appwrite-functions/check-cloudflare-status/package-lock.json new file mode 100644 index 0000000..a7e6739 --- /dev/null +++ b/appwrite-functions/check-cloudflare-status/package-lock.json @@ -0,0 +1,31 @@ +{ + "name": "check-cloudflare-status", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "check-cloudflare-status", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "node-appwrite": "^14.0.0" + } + }, + "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" + } + } +} diff --git a/appwrite-functions/check-cloudflare-status/package.json b/appwrite-functions/check-cloudflare-status/package.json new file mode 100644 index 0000000..4d83fcf --- /dev/null +++ b/appwrite-functions/check-cloudflare-status/package.json @@ -0,0 +1,10 @@ +{ + "name": "check-cloudflare-status", + "version": "1.0.0", + "type": "module", + "main": "src/index.js", + "license": "MIT", + "dependencies": { + "node-appwrite": "^14.0.0" + } +} diff --git a/appwrite-functions/check-cloudflare-status/src/index.js b/appwrite-functions/check-cloudflare-status/src/index.js new file mode 100644 index 0000000..fbadf91 --- /dev/null +++ b/appwrite-functions/check-cloudflare-status/src/index.js @@ -0,0 +1,107 @@ +import { Client, Databases } from 'node-appwrite'; + +const APPWRITE_ENDPOINT = process.env.APPWRITE_ENDPOINT || process.env.APPWRITE_FUNCTION_ENDPOINT; +const APPWRITE_PROJECT_ID = process.env.APPWRITE_PROJECT_ID || process.env.APPWRITE_FUNCTION_PROJECT_ID; +const APPWRITE_API_KEY = process.env.APPWRITE_API_KEY; +const DATABASE_ID = process.env.APPWRITE_DATABASE_ID; + +const cfHeaders = (token) => ({ + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' +}); + +async function fetchZones(token, log) { + const response = await fetch('https://api.cloudflare.com/client/v4/zones', { + headers: cfHeaders(token) + }); + + if (!response.ok) { + const body = await response.text(); + log(`Cloudflare zones error: ${body}`); + throw new Error('Failed to fetch Cloudflare zones'); + } + + const { result } = await response.json(); + return result.map((zone) => ({ + id: zone.id, + name: zone.name, + status: zone.status, + paused: zone.paused + })); +} + +async function fetchWorkers(token, accountId, log) { + if (!accountId) return []; + + const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts`, { + headers: cfHeaders(token) + }); + + if (!response.ok) { + const body = await response.text(); + log(`Cloudflare workers error: ${body}`); + throw new Error('Failed to fetch Cloudflare workers'); + } + + const { result } = await response.json(); + return result.map((worker) => ({ + name: worker.name, + modifiedOn: worker.modified_on, + active: worker.created_on !== undefined + })); +} + +export default async function ({ req, res, log, error }) { + try { + if (!APPWRITE_ENDPOINT || !APPWRITE_PROJECT_ID || !APPWRITE_API_KEY || !DATABASE_ID) { + return res.json({ error: 'Missing Appwrite environment configuration.' }, 500); + } + + const payload = req.body ? JSON.parse(req.body) : {}; + const accountId = payload.accountId; + const cloudflareAccountId = payload.cloudflareAccountId; + const requesterId = + (req.headers && (req.headers['x-appwrite-user-id'] || req.headers['x-appwrite-userid'])) || + process.env.APPWRITE_FUNCTION_USER_ID || + payload.userId; + + if (!accountId) { + return res.json({ error: 'accountId is required in the request body.' }, 400); + } + + const client = new Client() + .setEndpoint(APPWRITE_ENDPOINT) + .setProject(APPWRITE_PROJECT_ID) + .setKey(APPWRITE_API_KEY); + + const databases = new Databases(client); + const account = await databases.getDocument(DATABASE_ID, 'cloud_accounts', accountId); + + if (!account || account.provider !== 'cloudflare') { + return res.json({ error: 'Cloud account not found or not a Cloudflare credential.' }, 404); + } + + if (account.userId && requesterId && account.userId !== requesterId) { + return res.json({ error: 'You are not allowed to use this credential.' }, 403); + } + + const token = account.apiKey; + if (!token) { + return res.json({ error: 'Cloudflare token is missing for this account.' }, 400); + } + + const [zones, workers] = await Promise.all([ + fetchZones(token, log), + fetchWorkers(token, cloudflareAccountId, log) + ]); + + return res.json({ + zones, + workers, + message: workers.length ? 'Zones and Workers status fetched successfully.' : 'Zones status fetched successfully.' + }); + } catch (err) { + error(err.message); + return res.json({ error: 'Unexpected error while checking Cloudflare status.' }, 500); + } +} diff --git a/appwrite-functions/sync-github/function.json b/appwrite-functions/sync-github/function.json new file mode 100644 index 0000000..c4340ed --- /dev/null +++ b/appwrite-functions/sync-github/function.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://appwrite.io/docs/schemas/functions.json", + "name": "sync-github", + "entrypoint": "src/index.js", + "runtime": "node-20.0", + "commands": ["npm install"], + "ignore": ["node_modules", ".npm", "npm-debug.log", "build"] +} diff --git a/appwrite-functions/sync-github/package-lock.json b/appwrite-functions/sync-github/package-lock.json new file mode 100644 index 0000000..c3a7035 --- /dev/null +++ b/appwrite-functions/sync-github/package-lock.json @@ -0,0 +1,31 @@ +{ + "name": "sync-github", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sync-github", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "node-appwrite": "^14.0.0" + } + }, + "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" + } + } +} diff --git a/appwrite-functions/sync-github/package.json b/appwrite-functions/sync-github/package.json new file mode 100644 index 0000000..22ae47e --- /dev/null +++ b/appwrite-functions/sync-github/package.json @@ -0,0 +1,10 @@ +{ + "name": "sync-github", + "version": "1.0.0", + "type": "module", + "main": "src/index.js", + "license": "MIT", + "dependencies": { + "node-appwrite": "^14.0.0" + } +} diff --git a/appwrite-functions/sync-github/src/index.js b/appwrite-functions/sync-github/src/index.js new file mode 100644 index 0000000..83181f8 --- /dev/null +++ b/appwrite-functions/sync-github/src/index.js @@ -0,0 +1,76 @@ +import { Client, Databases } from 'node-appwrite'; + +const APPWRITE_ENDPOINT = process.env.APPWRITE_ENDPOINT || process.env.APPWRITE_FUNCTION_ENDPOINT; +const APPWRITE_PROJECT_ID = process.env.APPWRITE_PROJECT_ID || process.env.APPWRITE_FUNCTION_PROJECT_ID; +const APPWRITE_API_KEY = process.env.APPWRITE_API_KEY; +const DATABASE_ID = process.env.APPWRITE_DATABASE_ID; + +const githubHeaders = (token) => ({ + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json', + 'User-Agent': 'appwrite-sync-github' +}); + +export default async function ({ req, res, log, error }) { + try { + if (!APPWRITE_ENDPOINT || !APPWRITE_PROJECT_ID || !APPWRITE_API_KEY || !DATABASE_ID) { + return res.json({ error: 'Missing Appwrite environment configuration.' }, 500); + } + + const payload = req.body ? JSON.parse(req.body) : {}; + const accountId = payload.accountId; + const requesterId = + (req.headers && (req.headers['x-appwrite-user-id'] || req.headers['x-appwrite-userid'])) || + process.env.APPWRITE_FUNCTION_USER_ID || + payload.userId; + if (!accountId) { + return res.json({ error: 'accountId is required in the request body.' }, 400); + } + + const client = new Client() + .setEndpoint(APPWRITE_ENDPOINT) + .setProject(APPWRITE_PROJECT_ID) + .setKey(APPWRITE_API_KEY); + + const databases = new Databases(client); + const account = await databases.getDocument(DATABASE_ID, 'cloud_accounts', accountId); + + if (!account || account.provider !== 'github') { + return res.json({ error: 'Cloud account not found or not a GitHub credential.' }, 404); + } + + if (account.userId && requesterId && account.userId !== requesterId) { + return res.json({ error: 'You are not allowed to use this credential.' }, 403); + } + + const token = account.apiKey; + if (!token) { + return res.json({ error: 'GitHub token is missing for this account.' }, 400); + } + + const githubResponse = await fetch('https://api.github.com/user/repos?per_page=100', { + headers: githubHeaders(token) + }); + + if (!githubResponse.ok) { + const body = await githubResponse.text(); + log(`GitHub API error: ${body}`); + return res.json({ error: 'Failed to fetch repositories from GitHub.' }, githubResponse.status); + } + + const repositories = await githubResponse.json(); + const simplified = repositories.map((repo) => ({ + id: repo.id, + name: repo.name, + fullName: repo.full_name, + private: repo.private, + url: repo.html_url, + defaultBranch: repo.default_branch + })); + + return res.json({ repositories: simplified }, 200); + } catch (err) { + error(err.message); + return res.json({ error: 'Unexpected error while syncing with GitHub.' }, 500); + } +}