/** * n8n API Client * * Helper functions para gerenciar credenciais Google OAuth via n8n REST API * Referência: https://docs.n8n.io/api/ */ /** * Helper function to get API config with validation * Separated for testability */ function getApiConfig() { const baseUrl = process.env.N8N_API_URL || process.env.N8N_API_BASE_URL; const apiKey = process.env.N8N_API_KEY; if (!baseUrl || !apiKey) { throw new Error( "N8N_API_URL/N8N_API_BASE_URL e N8N_API_KEY não configurados", ); } return { apiBaseUrl: baseUrl, apiKey }; } /** * Buscar schema de um tipo de credencial * @param type - Nome do tipo de credencial (ex: 'googleOAuth2Api') * @returns Schema da credencial com campos obrigatórios */ export async function getCredentialSchema(type: string) { const { apiBaseUrl, apiKey } = getApiConfig(); const response = await fetch(`${apiBaseUrl}/credentials/schema/${type}`, { headers: { "X-N8N-API-KEY": apiKey, }, }); if (!response.ok) { throw new Error( `Failed to fetch credential schema: ${response.statusText}`, ); } return response.json(); } /** * Deletar credencial existente no n8n * * @param credentialId - ID da credencial a ser deletada * @returns true se deletado com sucesso */ export async function deleteCredential(credentialId: string): Promise { const { apiBaseUrl, apiKey } = getApiConfig(); 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; } } /** * Atualizar ou criar credencial Google OAuth no n8n * * Estratégia: DELETE + POST (não existe GET/PUT na API do n8n) * 1. Se oldCredentialId é fornecido, deleta a credencial antiga * 2. Cria nova credencial "refugio" com os dados atualizados * 3. Retorna o novo ID da credencial * * IMPORTANTE: Sempre deleta a credencial antiga antes de criar nova * para garantir que o nome "refugio" esteja sempre atualizado. * * @param credentialName - Nome da credencial (ex: "refugio") * @param clientId - Google Client ID * @param clientSecret - Google Client Secret * @param scopes - Escopos OAuth separados por espaço * @param accessToken - Access token do OAuth (opcional, para incluir tokens na criação) * @param refreshToken - Refresh token do OAuth (opcional, para incluir tokens na criação) * @param expiresIn - Tempo de expiração em segundos (opcional) * @param oldCredentialId - ID da credencial anterior para deletar (opcional) * @returns Credencial criada com novo ID */ export async function upsertGoogleCredential( credentialName: string, clientId: string, clientSecret: string, scopes: string, accessToken?: string, refreshToken?: string, expiresIn?: number, oldCredentialId?: string | null, ) { const { apiBaseUrl, apiKey } = getApiConfig(); // Passo 1: Deletar credencial antiga (se fornecido oldCredentialId ou se existe no .env) const credentialToDelete = oldCredentialId || process.env.N8N_GOOGLE_CREDENTIAL_ID; if (credentialToDelete) { console.log("[n8n-api] Deleting existing credential:", credentialToDelete); await deleteCredential(credentialToDelete); } else { console.log("[n8n-api] No existing credential to delete"); } // Passo 2: Preparar payload para nova credencial // Payload base com campos obrigatórios do schema googleCalendarOAuth2Api // Referência: n8n API schema validation (obtido via GET /credentials/schema/googleCalendarOAuth2Api) // NOTA: Apesar do schema indicar que sendAdditionalBodyProperties/additionalBodyProperties // são condicionais, a API requer eles SEMPRE. Valores default seguros abaixo. const basePayload = { name: credentialName, type: "googleCalendarOAuth2Api", data: { clientId, clientSecret, // Campos que schema indica como condicionais mas API requer sempre sendAdditionalBodyProperties: false, additionalBodyProperties: {}, // allowedHttpRequestDomains default "none" (não requer allowedDomains) allowedHttpRequestDomains: "none", }, }; // Se temos tokens, incluir no payload de criação const createPayload = accessToken ? { ...basePayload, data: { ...basePayload.data, oauthTokenData: { access_token: accessToken, refresh_token: refreshToken, scope: scopes, token_type: "Bearer", expiry_date: Date.now() + (expiresIn || 3599) * 1000, }, }, } : basePayload; // Passo 3: Criar nova credencial console.log('[n8n-api] Creating new credential "refugio"'); const createResponse = await fetch(`${apiBaseUrl}/credentials`, { method: "POST", headers: { "X-N8N-API-KEY": apiKey, "Content-Type": "application/json", }, body: JSON.stringify(createPayload), }); if (!createResponse.ok) { const error = await createResponse.json(); throw new Error(`Failed to create credential: ${JSON.stringify(error)}`); } const result = await createResponse.json(); console.log("[n8n-api] Credential created successfully:", result.id); console.log( "[n8n-api] IMPORTANTE: Atualize N8N_GOOGLE_CREDENTIAL_ID no .env.local para:", result.id, ); return result; } /** * Criar nova credencial Google Calendar OAuth2 no n8n * @param credentialName - Nome da credencial (ex: "refugio") * @param clientId - Google Client ID * @param clientSecret - Google Client Secret * @param scopes - Escopos OAuth separados por espaço (não usado no POST, apenas no oauthTokenData) * @returns Credencial criada com ID */ export async function createGoogleCredential( credentialName: string, clientId: string, clientSecret: string, _scopes: string, ) { const { apiBaseUrl, apiKey } = getApiConfig(); const payload = { name: credentialName, type: "googleCalendarOAuth2Api", data: { clientId, clientSecret, sendAdditionalBodyProperties: false, additionalBodyProperties: {}, allowedHttpRequestDomains: "none", }, }; const response = await fetch(`${apiBaseUrl}/credentials`, { method: "POST", headers: { "X-N8N-API-KEY": apiKey, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); if (!response.ok) { const error = await response.json(); throw new Error(`Failed to create credential: ${JSON.stringify(error)}`); } return response.json(); } /** * Atualizar tokens de uma credencial existente * @param credentialId - ID da credencial no n8n * @param accessToken - Access token do OAuth * @param refreshToken - Refresh token do OAuth * @param expiresIn - Tempo de expiração em segundos * @returns Credencial atualizada */ export async function updateCredentialTokens( credentialId: string, accessToken: string, refreshToken: string, expiresIn: number, ) { const { apiBaseUrl, apiKey } = getApiConfig(); const payload = { data: { oauthTokenData: { access_token: accessToken, refresh_token: refreshToken, token_type: "Bearer", expiry_date: Date.now() + expiresIn * 1000, }, }, }; const response = await fetch(`${apiBaseUrl}/credentials/${credentialId}`, { method: "PUT", headers: { "X-N8N-API-KEY": apiKey, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); if (!response.ok) { const error = await response.json(); throw new Error(`Failed to update credential: ${JSON.stringify(error)}`); } return response.json(); }