Dashboard-Automatizase/lib/n8n-api.ts
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

269 lines
7.8 KiB
TypeScript

/**
* 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<boolean> {
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();
}