- Updated Dockerfile to include hardcoded environment variables for Next.js build. - Enhanced Google Calendar API integration by extracting user email from id_token and adding scopes for OpenID and email access. - Modified credential management to delete existing credentials before creating new ones in n8n. - Updated dashboard to display connected Google Calendar email and credential details. Story: 4.2 - Melhorar integração com Google Calendar e atualizar Dockerfile 🤖 Generated with [Claude Code](https://claude.com/claude-code)
10 KiB
Estratégia de Gerenciamento de Credenciais n8n
Visão Geral
Este documento descreve a estratégia implementada para gerenciar credenciais Google OAuth no n8n via API REST, considerando as limitações da API.
Limitações da API n8n
A API REST do n8n para credenciais possui as seguintes limitações:
| Método HTTP | Endpoint | Disponível | Observação |
|---|---|---|---|
| GET | /credentials |
❌ NÃO | Método não permitido |
| GET | /credentials/{id} |
❌ NÃO | Não retorna dados por segurança |
| GET | /credentials/schema/{type} |
✅ SIM | Retorna apenas schema do tipo |
| POST | /credentials |
✅ SIM | Criar nova credencial |
| PUT | /credentials/{id} |
❌ NÃO | Método não permitido |
| DELETE | /credentials/{id} |
✅ SIM | Deletar credencial existente |
Fonte: n8n Community - Get/Update credentials via API
Estratégia Implementada: DELETE + POST
Como não é possível listar (GET) ou atualizar (PUT) credenciais, a estratégia adotada é:
Fluxo de upsertGoogleCredential()
1. Verificar se existe N8N_GOOGLE_CREDENTIAL_ID no .env.local
│
├─> SIM: Tentar deletar credencial antiga (DELETE /credentials/{id})
│ └─> Sucesso ou falha → Continuar para próximo passo
│
└─> NÃO: Pular para próximo passo
2. Criar nova credencial "refugio" (POST /credentials)
│
├─> Incluir clientId, clientSecret (obrigatório)
├─> Incluir oauthTokenData se tokens disponíveis (opcional)
│
└─> Retornar novo credential ID
3. Atualizar N8N_GOOGLE_CREDENTIAL_ID no .env.local (manual)
Vantagens
✅ Simplicidade: Não precisa gerenciar lógica complexa de verificação ✅ Idempotência: Sempre cria credencial "fresca" com dados atualizados ✅ Compatibilidade: Funciona com limitações da API do n8n ✅ Segurança: Tokens nunca são expostos via GET
Desvantagens
⚠️ ID muda: Cada OAuth gera um novo credential ID
⚠️ Atualização manual: Requer atualizar N8N_GOOGLE_CREDENTIAL_ID após cada OAuth (opcional)
⚠️ Credenciais órfãs: Credenciais antigas não são automaticamente limpas (mas são sobrescritas)
Implementação
Função Principal: upsertGoogleCredential()
Arquivo: lib/n8n-api.ts
export async function upsertGoogleCredential(
credentialName: string, // "refugio"
clientId: string, // Google Client ID
clientSecret: string, // Google Client Secret
scopes: string, // Escopos OAuth
accessToken?: string, // Access token (opcional)
refreshToken?: string, // Refresh token (opcional)
expiresIn?: number, // Expiração em segundos (opcional)
) {
// Passo 1: Deletar credencial antiga (se existir)
const existingCredentialId = process.env.N8N_GOOGLE_CREDENTIAL_ID;
if (existingCredentialId) {
await deleteCredential(existingCredentialId);
}
// Passo 2: Criar nova credencial
const result = await fetch(`${apiBaseUrl}/credentials`, {
method: "POST",
headers: {
"X-N8N-API-KEY": apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: credentialName,
type: "googleCalendarOAuth2Api",
data: {
clientId,
clientSecret,
sendAdditionalBodyProperties: false,
additionalBodyProperties: {},
allowedHttpRequestDomains: "none",
oauthTokenData: accessToken ? {
access_token: accessToken,
refresh_token: refreshToken,
scope: scopes,
token_type: "Bearer",
expiry_date: Date.now() + (expiresIn || 3599) * 1000,
} : undefined,
},
}),
});
// Passo 3: Retornar novo ID
const credential = await result.json();
console.log("IMPORTANTE: Atualize N8N_GOOGLE_CREDENTIAL_ID para:", credential.id);
return credential;
}
Função Auxiliar: deleteCredential()
export async function deleteCredential(credentialId: string): Promise<boolean> {
try {
const response = await fetch(`${apiBaseUrl}/credentials/${credentialId}`, {
method: "DELETE",
headers: {
"X-N8N-API-KEY": apiKey,
},
});
if (response.ok) {
console.log("[n8n-api] Credential deleted:", credentialId);
return true;
}
console.warn("[n8n-api] Failed to delete credential:", response.statusText);
return false;
} catch (error) {
console.error("[n8n-api] Error deleting credential:", error);
return false;
}
}
Uso no Fluxo OAuth
Callback Handler
Arquivo: app/api/google-calendar/callback/route.ts
// Após obter tokens do Google OAuth
const tokens = await tokenResponse.json();
// Chamar manage-credential para criar/atualizar credencial no n8n
const manageResponse = await fetch("/api/google-calendar/manage-credential", {
method: "POST",
body: JSON.stringify({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in || 3599,
}),
});
const manageResult = await manageResponse.json();
console.log("Credencial criada no n8n:", manageResult.credentialId);
Manage Credential API
Arquivo: app/api/google-calendar/manage-credential/route.ts
export async function POST(request: NextRequest) {
const { accessToken, refreshToken, expiresIn } = await request.json();
// Upsert credencial no n8n (DELETE + POST)
const result = await upsertGoogleCredential(
"refugio",
process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
process.env.GOOGLE_CLIENT_SECRET!,
"https://www.googleapis.com/auth/gmail.modify https://www.googleapis.com/auth/calendar.events",
accessToken,
refreshToken,
expiresIn,
);
// Salvar credential ID no Supabase
await supabase
.from("integrations")
.upsert({
user_id: user.id,
provider: "google_calendar",
status: "connected",
n8n_credential_id: result.id,
});
return NextResponse.json({ success: true, credentialId: result.id });
}
Variáveis de Ambiente
.env.local
# n8n API Configuration
N8N_API_URL=https://n8n.automatizase.com.br/api/v1
N8N_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# Google OAuth Configuration
NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-your-client-secret
# n8n Credential ID (opcional - será atualizado automaticamente após OAuth)
N8N_GOOGLE_CREDENTIAL_ID=
Fluxo Completo
1. Usuário clica "Conectar Google Calendar" no dashboard
│
2. Portal redireciona para Google OAuth
│
3. Usuário autoriza no Google
│
4. Google redireciona para /api/google-calendar/callback
│
5. Callback troca code por tokens (access_token + refresh_token)
│
6. Callback chama /api/google-calendar/manage-credential
│
7. manage-credential chama upsertGoogleCredential()
│
├─> DELETE credencial antiga (se N8N_GOOGLE_CREDENTIAL_ID existe)
├─> POST cria nova credencial "refugio" com tokens
└─> Retorna novo credential ID
│
8. manage-credential salva credential ID no Supabase
│
9. Dashboard redireciona para /dashboard?oauth_success=true
│
10. ✅ Credencial "refugio" está pronta no n8n com tokens válidos
Testes Unitários
Arquivo: lib/__tests__/n8n-api.test.ts
Cenários Cobertos
✅ deleteCredential - deve deletar credencial com sucesso
✅ deleteCredential - deve retornar false quando credencial não existe
✅ upsertGoogleCredential - deve deletar credencial existente e criar nova (DELETE + POST)
✅ upsertGoogleCredential - deve criar credencial via POST quando não existe N8N_GOOGLE_CREDENTIAL_ID
✅ upsertGoogleCredential - deve lançar erro quando POST falha após DELETE
✅ upsertGoogleCredential - deve continuar tentando criar mesmo se DELETE falhar
Executar Testes
npm test -- lib/__tests__/n8n-api.test.ts
Troubleshooting
Erro: "Failed to create credential"
Causa: Faltam variáveis de ambiente ou payload inválido
Solução:
- Verifique se
N8N_API_URLeN8N_API_KEYestão configurados - Verifique se
NEXT_PUBLIC_GOOGLE_CLIENT_IDeGOOGLE_CLIENT_SECRETestão configurados - Execute:
bash tmp/test-n8n-schema.shpara validar API
Credenciais antigas não deletadas
Causa: N8N_GOOGLE_CREDENTIAL_ID não está atualizado
Solução:
- Após OAuth bem-sucedido, verifique os logs do servidor
- Copie o novo credential ID mostrado nos logs
- Atualize
N8N_GOOGLE_CREDENTIAL_IDno.env.local - Reinicie o servidor
Credential ID muda após cada OAuth
Comportamento esperado: A estratégia DELETE + POST sempre cria um novo ID.
Alternativas:
- Armazenar credential ID no Supabase (já implementado)
- Usar credential ID do Supabase em vez de
.env.local(melhoria futura)
Melhorias Futuras
1. Buscar Credential ID do Supabase
Em vez de usar process.env.N8N_GOOGLE_CREDENTIAL_ID, buscar do banco:
const { data } = await supabase
.from("integrations")
.select("n8n_credential_id")
.eq("user_id", user.id)
.eq("provider", "google_calendar")
.single();
const existingCredentialId = data?.n8n_credential_id;
2. Limpeza Automática de Credenciais Órfãs
Adicionar job periódico para deletar credenciais não usadas.
3. Cache de Credential ID
Armazenar credential ID em memória durante sessão para reduzir queries ao Supabase.
Referências
- n8n API Documentation
- n8n Community - Get/Update credentials via API
- Story 3.4 - Gerenciar Credenciais n8n
- Google OAuth Setup
Changelog
| Data | Versão | Alteração |
|---|---|---|
| 2025-10-12 | 1.0.0 | Implementação inicial da estratégia DELETE + POST |