- 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)
269 lines
7.8 KiB
TypeScript
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();
|
|
}
|