UNPKG

n8n-nodes-arubacentral

Version:

n8n community node for Aruba Central API integration with comprehensive monitoring, configuration, and management capabilities

526 lines (525 loc) 23.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getAccessToken = getAccessToken; exports.apiRequest = apiRequest; // helpers/apiRequest.ts const n8n_workflow_1 = require("n8n-workflow"); const logger_1 = require("./logger"); const tokenCache = {}; let sessionToken; let csrfToken; /** * Extract a token from cookies */ function extractTokenFromCookies(cookies, tokenName) { if (!cookies || cookies.length === 0) { throw new Error(`No cookies found when extracting ${tokenName}`); } for (const cookie of cookies) { const match = new RegExp(`${tokenName}=([^;]+)`).exec(cookie); if (match) { return match[1]; } } throw new Error(`${tokenName} not found in cookies`); } /** * Check if in rate limit backoff period */ function isRateLimited(cacheKey) { var _a, _b; if (((_a = tokenCache[cacheKey]) === null || _a === void 0 ? void 0 : _a.retryAfter) && ((_b = tokenCache[cacheKey]) === null || _b === void 0 ? void 0 : _b.retryTimestamp)) { const now = Date.now(); const retryTime = tokenCache[cacheKey].retryTimestamp + tokenCache[cacheKey].retryAfter * 1000; if (now < retryTime) { return { isLimited: true, waitTime: Math.ceil((retryTime - now) / 1000), }; } } return { isLimited: false, waitTime: 0 }; } /** * Handle rate limiting response */ function handleRateLimiting(cacheKey, response) { // Parse retry time from response let retryAfter = 60; // Default 60 seconds try { // Check if retryAfter was directly provided if (response.retryAfter && typeof response.retryAfter === 'number') { retryAfter = response.retryAfter; } // Check for retry-after header else if (response.headers && response.headers['retry-after']) { retryAfter = parseInt(response.headers['retry-after'], 10); } // Try to parse from error message else { const message = (response === null || response === void 0 ? void 0 : response.message) || (response === null || response === void 0 ? void 0 : response.error_description) || ''; const match = message.match(/retry after (\d+) seconds/i) || message.match(/(\d+) seconds/); if (match && match[1]) { retryAfter = parseInt(match[1], 10); } } } catch (e) { logger_1.logger.error('auth:ratelimit', `Failed to parse retry time: ${e.message}`); } // Store rate limit info in cache if (tokenCache[cacheKey]) { tokenCache[cacheKey].retryAfter = retryAfter; tokenCache[cacheKey].retryTimestamp = Date.now(); } else { tokenCache[cacheKey] = { accessToken: '', refreshToken: '', expiresAt: 0, retryAfter, retryTimestamp: Date.now(), }; } logger_1.logger.warn('auth:ratelimit', `Rate limited. Will retry after ${retryAfter} seconds`); } /** * Get token data from n8n storage */ function getTokenFromCredentials(credentials) { logger_1.logger.debug('auth:storage', 'Checking for token in n8n credential storage'); try { if (credentials.oauthTokenData) { const tokenData = credentials.oauthTokenData; logger_1.logger.debug('auth:storage', 'Token data found in credentials'); if (tokenData.access_token && tokenData.expires_at) { logger_1.logger.debug('auth:storage', 'Found access token and expiration'); return { accessToken: tokenData.access_token, refreshToken: tokenData.refresh_token || '', expiresAt: tokenData.expires_at, }; } } logger_1.logger.debug('auth:storage', 'No valid token data found in credential storage'); return null; } catch (error) { logger_1.logger.error('auth:storage', `Error retrieving token from credentials: ${error.message}`); return null; } } /** * Store OAuth token data in n8n credentials or workflow storage for persistence */ async function storeTokenData(tokenData, credentialId) { try { // Calculate expiration timestamp const expiresAt = Date.now() + tokenData.expires_in * 1000; // Format token data for storage const oauthTokenData = { access_token: tokenData.access_token, refresh_token: tokenData.refresh_token, expires_in: tokenData.expires_in, expires_at: expiresAt, token_type: 'Bearer', }; // Try different methods to store the token based on available n8n APIs let stored = false; // Method 1: Try using nodeHelpers if available (newer n8n versions) try { if (this.helpers.nodeHelpers && typeof this.helpers.nodeHelpers.updateCredentials === 'function') { await this.helpers.nodeHelpers.updateCredentials(credentialId, { oauthTokenData, }); logger_1.logger.debug('auth:oauth2', 'Token stored using nodeHelpers.updateCredentials'); stored = true; } } catch (error) { logger_1.logger.debug('auth:oauth2', `nodeHelpers.updateCredentials failed: ${error.message}`); } // Method 2: Try direct updateCredentials if available (older n8n versions) if (!stored && typeof this.helpers.updateCredentials === 'function') { try { await this.helpers.updateCredentials(credentialId, { oauthTokenData, }); logger_1.logger.debug('auth:oauth2', 'Token stored using helpers.updateCredentials'); stored = true; } catch (error) { logger_1.logger.debug('auth:oauth2', `helpers.updateCredentials failed: ${error.message}`); } } // Method 3: Fall back to workflow static data if credential update is not possible if (!stored && typeof this.getWorkflowStaticData === 'function') { const workflowStaticData = this.getWorkflowStaticData('node'); workflowStaticData.oauthTokenData = oauthTokenData; logger_1.logger.debug('auth:oauth2', 'Token stored in workflow static data (fallback method)'); stored = true; } if (!stored) { logger_1.logger.warn('auth:oauth2', 'Could not persist token data - no suitable storage method available'); } } catch (error) { logger_1.logger.error('auth:oauth2', `Failed to persist token data: ${error.message}`); // We continue without throwing as not persisting is better than failing completely } } /** * Check if a token is expired */ function isTokenExpired(expiresAt) { // Consider token expired 60 seconds before actual expiration return Date.now() > expiresAt - 60000; } /** * Get an access token using the OAuth2 flow */ async function getAccessToken(credentials) { var _a, _b, _c, _d; logger_1.logger.debug('auth:oauth2', 'Getting access token...'); const cacheKey = `${credentials.baseUrl}_${credentials.clientId}`; const credentialId = credentials.$credentialId; // Check rate limiting first const rateLimitStatus = isRateLimited(cacheKey); if (rateLimitStatus.isLimited) { throw new Error(`API rate limited. Please try again after ${rateLimitStatus.waitTime} seconds.`); } // First try workflow static data as a persistent token source if (typeof this.getWorkflowStaticData === 'function') { const workflowStaticData = this.getWorkflowStaticData('node'); if (workflowStaticData.oauthTokenData) { const tokenData = workflowStaticData.oauthTokenData; if (tokenData.access_token && tokenData.expires_at && Date.now() < tokenData.expires_at) { logger_1.logger.debug('auth:oauth2', 'Using valid token from workflow static data'); return tokenData.access_token; } else { logger_1.logger.debug('auth:oauth2', 'Token in workflow static data is expired or invalid'); } } } // Second, try to get token from n8n credential storage const storedToken = getTokenFromCredentials(credentials); logger_1.logger.debug('auth:oauth2', storedToken ? 'Found token in credential storage' : 'No token in credential storage'); if (storedToken && !isTokenExpired(storedToken.expiresAt)) { logger_1.logger.debug('auth:oauth2', 'Using valid access token from n8n credential storage'); // Update runtime cache for faster access next time tokenCache[cacheKey] = { accessToken: storedToken.accessToken, refreshToken: storedToken.refreshToken, expiresAt: storedToken.expiresAt, }; return storedToken.accessToken; } // Third, check in-memory cache if (tokenCache[cacheKey] && !isTokenExpired(tokenCache[cacheKey].expiresAt)) { logger_1.logger.debug('auth:oauth2', 'Using valid token from in-memory cache'); return tokenCache[cacheKey].accessToken; } // Fourth, try to refresh the token if we have a refresh token const refreshToken = (storedToken === null || storedToken === void 0 ? void 0 : storedToken.refreshToken) || ((_a = tokenCache[cacheKey]) === null || _a === void 0 ? void 0 : _a.refreshToken); if (refreshToken) { logger_1.logger.debug('auth:oauth2', 'Attempting to refresh token'); try { const refreshResponse = await this.helpers.request({ method: 'POST', uri: `${credentials.baseUrl}/oauth2/token`, qs: { client_id: credentials.clientId, client_secret: credentials.clientSecret, grant_type: 'refresh_token', refresh_token: refreshToken, }, json: true, }); logger_1.logger.debug('auth:oauth2', 'Token refresh successful'); // Format and store the new token const expiresAt = Date.now() + refreshResponse.expires_in * 1000; // Update the runtime cache tokenCache[cacheKey] = { accessToken: refreshResponse.access_token, refreshToken: refreshResponse.refresh_token, expiresAt, }; // Persist tokens await storeTokenData.call(this, { access_token: refreshResponse.access_token, refresh_token: refreshResponse.refresh_token, expires_in: refreshResponse.expires_in, }, credentialId); logger_1.logger.debug('auth:oauth2', 'New token stored after refresh'); return refreshResponse.access_token; } catch (error) { logger_1.logger.error('auth:oauth2', `Token refresh failed: ${error.message}`); logger_1.logger.debug('auth:oauth2', 'Proceeding to full authentication flow'); // Check for rate limiting in refresh error if (error.statusCode === 429) { handleRateLimiting(cacheKey, error.error); throw new Error(`API rate limited during token refresh. Please try again later.`); } } } // Full OAuth flow logger_1.logger.debug('auth:oauth2', 'Starting full OAuth flow...'); try { // Step 1: Login to obtain session and CSRF tokens. const loginUrl = `${credentials.baseUrl}/oauth2/authorize/central/api/login?client_id=${credentials.clientId}`; logger_1.logger.debug('auth:oauth2', `Calling login endpoint: ${loginUrl}`); const loginResponse = await this.helpers.request({ method: 'POST', uri: loginUrl, headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: { username: credentials.username, password: credentials.password, }, json: true, resolveWithFullResponse: true, }); logger_1.logger.debug('auth:oauth2', 'Login response received'); // Extract tokens from response cookies. try { sessionToken = extractTokenFromCookies(loginResponse.headers['set-cookie'], 'session'); csrfToken = extractTokenFromCookies(loginResponse.headers['set-cookie'], 'csrftoken'); logger_1.logger.debug('auth:oauth2', 'Session and CSRF tokens extracted successfully'); } catch (error) { logger_1.logger.error('auth:oauth2', `Failed to extract tokens: ${error.message}`); throw new Error(`Authentication failed: ${error.message}`); } // Step 2: Generate authorization code. const authCodeUrl = `${credentials.baseUrl}/oauth2/authorize/central/api?client_id=${credentials.clientId}&response_type=code&scope=all`; logger_1.logger.debug('auth:oauth2', `Calling authorization code endpoint: ${authCodeUrl}`); const authCodeResponse = await this.helpers.request({ method: 'POST', uri: authCodeUrl, headers: { 'Content-Type': 'application/json', Cookie: `session=${sessionToken}`, 'X-CSRF-Token': csrfToken, }, body: { customer_id: credentials.customerId, }, json: true, }); if (!authCodeResponse || !authCodeResponse.auth_code) { logger_1.logger.error('auth:oauth2', 'No authorization code in response', authCodeResponse); throw new Error('Failed to obtain authorization code'); } const authorizationCode = authCodeResponse.auth_code; logger_1.logger.debug('auth:oauth2', 'Authorization code obtained successfully'); // Step 3: Exchange the authorization code for an access token. const tokenUrl = `${credentials.baseUrl}/oauth2/token`; logger_1.logger.debug('auth:oauth2', `Exchanging authorization code for access token at: ${tokenUrl}`); const tokenResponse = await this.helpers.request({ method: 'POST', uri: tokenUrl, headers: { 'Content-Type': 'application/json', }, body: { client_id: credentials.clientId, client_secret: credentials.clientSecret, grant_type: 'authorization_code', code: authorizationCode, }, json: true, }); if (!tokenResponse || !tokenResponse.access_token) { logger_1.logger.error('auth:oauth2', 'No access token in response', tokenResponse); throw new Error('Failed to obtain access token'); } // Calculate expiration const expiresAt = Date.now() + tokenResponse.expires_in * 1000; // Store in runtime cache tokenCache[cacheKey] = { accessToken: tokenResponse.access_token, refreshToken: tokenResponse.refresh_token, expiresAt, }; // Store persistently await storeTokenData.call(this, { access_token: tokenResponse.access_token, refresh_token: tokenResponse.refresh_token, expires_in: tokenResponse.expires_in, }, credentialId); logger_1.logger.debug('auth:oauth2', 'Full OAuth flow completed successfully'); return tokenResponse.access_token; } catch (error) { logger_1.logger.error('auth:oauth2', `OAuth authentication failed: ${error.message}`); // Enhanced rate limiting detection if (error.statusCode === 429 || (((_b = error.response) === null || _b === void 0 ? void 0 : _b.body) && typeof error.response.body === 'object' && error.response.body.message && error.response.body.message.includes('rate limit'))) { // Extract retry time let retryAfter = '60'; let errorMsg = ''; if (((_c = error.response) === null || _c === void 0 ? void 0 : _c.headers) && error.response.headers['retry-after']) { retryAfter = error.response.headers['retry-after']; } else if (((_d = error.response) === null || _d === void 0 ? void 0 : _d.body) && typeof error.response.body === 'object') { errorMsg = error.response.body.message || ''; const match = errorMsg.match(/(\d+) seconds/); if (match && match[1]) { retryAfter = match[1]; } } else if (typeof error.error === 'object' && error.error.message) { errorMsg = error.error.message; const match = errorMsg.match(/(\d+) seconds/); if (match && match[1]) { retryAfter = match[1]; } } const retrySeconds = parseInt(retryAfter, 10) || 60; handleRateLimiting(cacheKey, { message: errorMsg, retryAfter: retrySeconds, }); throw new Error(`API rate limited during authentication. Please try again after ${retrySeconds} seconds.`); } throw new Error(`Authentication failed: ${error.message}`); } } /** * Make an API request to Aruba Central with OAuth2 authentication */ async function apiRequest(method, endpoint, body = {}, qs = {}) { var _a; logger_1.logger.debug('api:request', `${method} ${endpoint} started`); logger_1.logger.debug('api:request:params', JSON.stringify(qs, null, 2)); const credentials = await this.getCredentials('ArubaCentralOAuth2Api'); if (!credentials) { logger_1.logger.error('api:request', 'No credentials provided'); throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'No credentials provided'); } logger_1.logger.debug('api:request', 'Credentials loaded successfully'); const baseUrl = credentials.baseUrl; // Print credential object structure (without sensitive values) const credentialKeys = Object.keys(credentials); logger_1.logger.debug('api:request:credentials', `Available credential keys: ${JSON.stringify(credentialKeys)}`); if (credentials.oauthTokenData) { const tokenDataKeys = Object.keys(credentials.oauthTokenData); logger_1.logger.debug('api:request:credentials', `Token data keys: ${JSON.stringify(tokenDataKeys)}`); } else { logger_1.logger.debug('api:request:credentials', 'No oauthTokenData found in credentials'); } try { // Get access token using the OAuth2 flow logger_1.logger.debug('api:request', 'Getting access token'); const accessToken = await getAccessToken.call(this, credentials); logger_1.logger.debug('api:request', 'Successfully obtained access token'); // Make the actual API request logger_1.logger.debug('api:request', `Making ${method} request to ${endpoint}`); const requestOptions = { method, url: `${baseUrl}${endpoint}`, headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, body, qs, json: true, resolveWithFullResponse: true, }; logger_1.logger.debug('api:request:options', JSON.stringify({ method: requestOptions.method, url: requestOptions.url, headers: { 'Content-Type': (_a = requestOptions.headers) === null || _a === void 0 ? void 0 : _a['Content-Type'], Authorization: '***', }, qs: requestOptions.qs, body: requestOptions.body, }, null, 2)); // Use n8n's request helper const response = await this.helpers.httpRequest(requestOptions); logger_1.logger.debug('api:request:response', `Response received`); return response.body || response; } catch (error) { logger_1.logger.error('api:request:exception', error.message); // Handle API-specific errors if (error.response) { logger_1.logger.error('api:request:error', `Status: ${error.statusCode}, Body: ${JSON.stringify(error.error)}`); let message = 'Unknown error'; const errorBody = error.error || {}; // More detailed error message extraction if (typeof errorBody === 'object') { if (errorBody.description) { message = errorBody.description; } else if (errorBody.error_description) { message = errorBody.error_description; } else if (errorBody.message) { message = errorBody.message; } else if (errorBody.error) { message = errorBody.error; } } else if (typeof errorBody === 'string') { try { const parsedBody = JSON.parse(errorBody); if (parsedBody.message) { message = parsedBody.message; } } catch (e) { // If parsing fails, use the string if it's not too long if (errorBody.length < 300) { message = errorBody; } } } logger_1.logger.error('api:request:error', `Formatted message: ${message}`); // Handle rate limiting specifically if (error.statusCode === 429) { const cacheKey = `${credentials.baseUrl}_${credentials.clientId}`; // Extract retry time let retryAfter = '60'; if (error.response.headers && error.response.headers['retry-after']) { retryAfter = error.response.headers['retry-after']; } else if (message.includes('seconds')) { const match = message.match(/(\d+) seconds/); if (match && match[1]) { retryAfter = match[1]; } } // Use the extracted retry time or a default const retrySeconds = parseInt(retryAfter, 10) || 60; handleRateLimiting(cacheKey, { message, retryAfter: retrySeconds, }); throw new Error(`API rate limited. Please try again after ${retrySeconds} seconds.`); } throw new n8n_workflow_1.NodeApiError(this.getNode(), error, { message }); } // Re-throw with original error logger_1.logger.error('api:request:error', 'Re-throwing original error'); throw error; } finally { logger_1.logger.debug('api:request', 'Request completed'); } }