Dashboard-Automatizase/docs/n8n-credential-management-strategy.md
Luis 1391fe6216 feat: enhance Google Calendar integration and update Dockerfile for environment variables
- 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)
2025-10-12 21:16:21 -03:00

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:

  1. Verifique se N8N_API_URL e N8N_API_KEY estão configurados
  2. Verifique se NEXT_PUBLIC_GOOGLE_CLIENT_ID e GOOGLE_CLIENT_SECRET estão configurados
  3. Execute: bash tmp/test-n8n-schema.sh para validar API

Credenciais antigas não deletadas

Causa: N8N_GOOGLE_CREDENTIAL_ID não está atualizado

Solução:

  1. Após OAuth bem-sucedido, verifique os logs do servidor
  2. Copie o novo credential ID mostrado nos logs
  3. Atualize N8N_GOOGLE_CREDENTIAL_ID no .env.local
  4. 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


Changelog

Data Versão Alteração
2025-10-12 1.0.0 Implementação inicial da estratégia DELETE + POST