corrad-bp/server/api/process/workflow-api-call.post.js
Afiq bae98c2b17 Enhance Workflow API Call Handling and Authorization Logic
- Introduced a new proxy endpoint for API calls during workflow execution to handle CORS issues and streamline API interactions.
- Updated the authorization logic to support Basic Auth with both token and username/password options, improving flexibility in API authentication.
- Enhanced the API request building process to accommodate new node data structures, including dynamic handling of headers, parameters, and body content.
- Improved error handling and response management in the workflow execution process, ensuring better feedback and control over API call outcomes.
- Refactored the workflow page to utilize the new API call structure, enhancing overall workflow execution reliability and user experience.
2025-08-04 12:50:54 +08:00

198 lines
6.7 KiB
JavaScript

/**
* Workflow API Call Proxy Endpoint
*
* This endpoint acts as a proxy for API calls made during workflow execution.
* It handles the new API node structure with proper authorization and avoids CORS issues.
*/
// Helper function to substitute variables in a string
function substituteVariables(str, variables) {
if (typeof str !== 'string') return str;
// Replace {{variable}} first
str = str.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (match, varName) => {
const value = variables[varName];
if (value === undefined || value === null) return '';
if (typeof value === 'object') {
return JSON.stringify(value);
}
return String(value);
});
// Then replace {variable}
str = str.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, varName) => {
const value = variables[varName];
if (value === undefined || value === null) return '';
if (typeof value === 'object') {
return JSON.stringify(value);
}
return String(value);
});
return str;
}
// Build API request from node data
function buildApiRequest(nodeData, variables) {
// 1. URL (with param substitution)
let url = substituteVariables(nodeData.apiUrl, variables);
// 2. Params (for GET, DELETE, etc.)
let params = Array.isArray(nodeData.params) ? nodeData.params : [];
if (params.length) {
const query = params
.filter(p => p.key)
.map(p => `${encodeURIComponent(substituteVariables(p.key, variables))}=${encodeURIComponent(substituteVariables(p.value, variables))}`)
.join('&');
if (query) {
url += (url.includes('?') ? '&' : '?') + query;
}
}
// 3. Headers
let headers = {};
if (Array.isArray(nodeData.headers)) {
nodeData.headers.forEach(h => {
if (h.key) headers[substituteVariables(h.key, variables)] = substituteVariables(h.value, variables);
});
} else if (typeof nodeData.headers === 'object') {
headers = { ...nodeData.headers };
}
// 4. Authorization
if (nodeData.authorization && nodeData.authorization.type && nodeData.authorization.type !== 'none') {
const auth = nodeData.authorization;
if (auth.type === 'bearer' && auth.token) {
headers['Authorization'] = `Bearer ${substituteVariables(auth.token, variables)}`;
} else if (auth.type === 'basic') {
if (auth.token) {
// Basic Auth with token (JWT or other token)
headers['Authorization'] = `Basic ${substituteVariables(auth.token, variables)}`;
} else if (auth.username && auth.password) {
// Basic Auth with username/password
const token = Buffer.from(`${substituteVariables(auth.username, variables)}:${substituteVariables(auth.password, variables)}`).toString('base64');
headers['Authorization'] = `Basic ${token}`;
}
} else if (auth.type === 'apiKey' && auth.key && auth.value) {
if (auth.in === 'header') {
headers[substituteVariables(auth.key, variables)] = substituteVariables(auth.value, variables);
} else if (auth.in === 'query') {
url += (url.includes('?') ? '&' : '?') + `${encodeURIComponent(substituteVariables(auth.key, variables))}=${encodeURIComponent(substituteVariables(auth.value, variables))}`;
}
}
}
// 5. Body
let body;
if (nodeData.body && nodeData.body.type && nodeData.body.type !== 'none') {
if (['form-data', 'x-www-form-urlencoded'].includes(nodeData.body.type)) {
const dataArr = Array.isArray(nodeData.body.data) ? nodeData.body.data : [];
if (nodeData.body.type === 'form-data') {
// For server-side, we'll use URLSearchParams for form-data
const formData = new URLSearchParams();
dataArr.forEach(item => {
if (item.key) formData.append(substituteVariables(item.key, variables), substituteVariables(item.value, variables));
});
body = formData.toString();
headers['Content-Type'] = 'application/x-www-form-urlencoded';
} else {
// x-www-form-urlencoded
body = dataArr
.filter(item => item.key)
.map(item => `${encodeURIComponent(substituteVariables(item.key, variables))}=${encodeURIComponent(substituteVariables(item.value, variables))}`)
.join('&');
headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
} else if (nodeData.body.type === 'raw') {
body = substituteVariables(nodeData.body.data, variables);
// Try to detect JSON
if (body && body.trim().startsWith('{')) {
headers['Content-Type'] = 'application/json';
}
}
}
return { url, headers, body };
}
export default defineEventHandler(async (event) => {
try {
// Get request body
const body = await readBody(event);
// Extract node configuration and process variables
const { nodeData, processVariables } = body;
// Validate input
if (!nodeData || !nodeData.apiUrl) {
return {
success: false,
error: {
message: 'Invalid API node configuration. Missing apiUrl.'
}
};
}
// Build the API request
const { url, headers, body: requestBody } = buildApiRequest(nodeData, processVariables);
const apiMethod = nodeData.apiMethod || 'GET';
const outputVariable = nodeData.outputVariable || 'apiResponse';
const errorVariable = nodeData.errorVariable || 'apiError';
const continueOnError = nodeData.continueOnError || false;
// Prepare fetch options
const fetchOptions = {
method: apiMethod,
headers
};
// Add body for non-GET requests
if (!['GET', 'HEAD'].includes(apiMethod) && requestBody) {
fetchOptions.body = requestBody;
}
// Make the API call
const response = await fetch(url, fetchOptions);
// Get response data
let responseData;
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
responseData = await response.json();
} else {
responseData = await response.text();
}
// Prepare result
const result = {
success: response.ok,
data: responseData,
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries([...response.headers.entries()])
};
if (!response.ok) {
result.error = {
message: `API call failed with status ${response.status}`,
status: response.status,
statusText: response.statusText,
data: responseData
};
}
return result;
} catch (error) {
return {
success: false,
error: {
message: error.message || 'An error occurred while making the API call',
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
}
};
}
});