Merge pull request #3 from rede5/codex/create-github-sync-cloud-function
Add Appwrite proxy functions and security rules
This commit is contained in:
commit
519c00643d
10 changed files with 313 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
npm-debug.log
|
||||
30
SECURITY.md
Normal file
30
SECURITY.md
Normal file
|
|
@ -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.
|
||||
8
appwrite-functions/check-cloudflare-status/function.json
Normal file
8
appwrite-functions/check-cloudflare-status/function.json
Normal file
|
|
@ -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"]
|
||||
}
|
||||
31
appwrite-functions/check-cloudflare-status/package-lock.json
generated
Normal file
31
appwrite-functions/check-cloudflare-status/package-lock.json
generated
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
appwrite-functions/check-cloudflare-status/package.json
Normal file
10
appwrite-functions/check-cloudflare-status/package.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
107
appwrite-functions/check-cloudflare-status/src/index.js
Normal file
107
appwrite-functions/check-cloudflare-status/src/index.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
8
appwrite-functions/sync-github/function.json
Normal file
8
appwrite-functions/sync-github/function.json
Normal file
|
|
@ -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"]
|
||||
}
|
||||
31
appwrite-functions/sync-github/package-lock.json
generated
Normal file
31
appwrite-functions/sync-github/package-lock.json
generated
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
appwrite-functions/sync-github/package.json
Normal file
10
appwrite-functions/sync-github/package.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
76
appwrite-functions/sync-github/src/index.js
Normal file
76
appwrite-functions/sync-github/src/index.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue