diff --git a/lib/services/n8n-service.ts b/lib/services/n8n-service.ts index dcdae1be..81e5b788 100644 --- a/lib/services/n8n-service.ts +++ b/lib/services/n8n-service.ts @@ -1,75 +1,42 @@ -import axios from 'axios'; +import { env } from '@/lib/env'; export class N8nService { private webhookUrl: string; private rollbackWebhookUrl: string; constructor() { + // Use consistent webhook URLs without -test suffix this.webhookUrl = process.env.N8N_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/mission-created'; this.rollbackWebhookUrl = process.env.N8N_ROLLBACK_WEBHOOK_URL || 'https://brain.slm-lab.net/webhook/mission-rollback'; - console.log('N8nService initialized with webhook URLs:', { - create: this.webhookUrl, - rollback: this.rollbackWebhookUrl - }); } - /** - * Trigger the mission creation workflow in n8n - * @param missionData The mission data to process - * @returns The workflow execution result - */ - async createMission(missionData: any): Promise { + async triggerMissionCreation(data: any): Promise { try { - console.log('Triggering n8n workflow for mission creation:', { - name: missionData.name, - missionType: missionData.missionType, - webhookUrl: this.webhookUrl, - fullData: JSON.stringify(missionData, null, 2) - }); + console.log('Triggering n8n workflow with data:', JSON.stringify(data, null, 2)); + console.log('Using webhook URL:', this.webhookUrl); - const response = await axios.post(this.webhookUrl, missionData, { + const response = await fetch(this.webhookUrl, { + method: 'POST', headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json' }, - timeout: 30000 // 30 second timeout + body: JSON.stringify(data), }); - console.log('n8n workflow response:', { - status: response.status, - statusText: response.statusText, - headers: response.headers, - data: response.data, - contentType: response.headers['content-type'] - }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } - // Handle string response - if (typeof response.data === 'string') { - console.warn('Received string response from n8n instead of JSON:', { - response: response.data, - contentType: response.headers['content-type'], - webhookUrl: this.webhookUrl - }); - - // Try to parse the string as JSON if it looks like JSON - if (response.data.trim().startsWith('{') || response.data.trim().startsWith('[')) { - try { - const parsedData = JSON.parse(response.data); - console.log('Successfully parsed string response as JSON:', parsedData); - return { - success: true, - results: parsedData - }; - } catch (parseError) { - console.error('Failed to parse string response as JSON:', parseError); - } - } + const result = await response.json(); + console.log('Received response from n8n:', JSON.stringify(result, null, 2)); - // If we can't parse it as JSON, return default structure + // Handle different response formats + if (typeof result === 'string') { + console.warn('Received string response from n8n:', result); return { - success: true, + success: false, + error: 'Invalid response format from n8n', results: { - message: response.data, leantimeProjectId: null, outlineCollectionId: null, rocketChatChannelId: null, @@ -78,102 +45,64 @@ export class N8nService { }; } - if (response.data.errors && response.data.errors.length > 0) { - console.warn('Workflow completed with partial success:', response.data.errors); - return response.data; - } + // Extract results from the response + const integrationResults = result.results || result; + console.log('Integration results:', JSON.stringify(integrationResults, null, 2)); - if (!response.data.success) { - console.error('Workflow execution failed:', { - message: response.data.message, - data: response.data - }); - throw new Error(`Workflow execution failed: ${response.data.message || 'Unknown error'}`); - } - - // Ensure the response has the expected structure - const result = response.data; - if (!result.results) { - console.warn('Response missing results object, adding default structure'); - result.results = { + return { + success: true, + results: { + leantimeProjectId: integrationResults.leantimeProjectId?.toString() || null, + outlineCollectionId: integrationResults.outlineCollectionId?.toString() || null, + rocketChatChannelId: integrationResults.rocketChatChannelId?.toString() || null, + giteaRepositoryUrl: integrationResults.giteaRepositoryUrl || null + } + }; + } catch (error) { + console.error('Error triggering n8n workflow:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + results: { leantimeProjectId: null, outlineCollectionId: null, rocketChatChannelId: null, giteaRepositoryUrl: null - }; - } - - return result; - } catch (error) { - console.error('Error triggering n8n workflow:', { - error: error instanceof Error ? error.message : String(error), - webhookUrl: this.webhookUrl, - errorDetails: error instanceof Error ? error.stack : undefined - }); - throw new Error(`Failed to trigger mission creation workflow: ${error instanceof Error ? error.message : String(error)}`); + } + }; } } - /** - * Trigger the mission rollback workflow in n8n - * @param mission The mission to rollback - * @returns The workflow execution result - */ - async rollbackMission(mission: any): Promise { + async triggerMissionRollback(data: any): Promise { try { - console.log('Triggering n8n workflow for mission rollback:', { - id: mission.id, - name: mission.name, - webhookUrl: this.rollbackWebhookUrl, - fullData: JSON.stringify(mission, null, 2) - }); + console.log('Triggering n8n rollback workflow with data:', JSON.stringify(data, null, 2)); + console.log('Using rollback webhook URL:', this.rollbackWebhookUrl); - const response = await axios.post(this.rollbackWebhookUrl, mission, { + const response = await fetch(this.rollbackWebhookUrl, { + method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', }, - timeout: 30000 // 30 second timeout + body: JSON.stringify(data), }); - console.log('n8n rollback workflow response:', { - status: response.status, - statusText: response.statusText, - headers: response.headers, - data: response.data - }); - - // Handle string response - if (typeof response.data === 'string') { - console.log('Received string response from n8n rollback, treating as success'); - return { - success: true, - results: { - message: response.data - } - }; + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); } - if (response.data.errors && response.data.errors.length > 0) { - console.warn('Rollback workflow completed with partial success:', response.data.errors); - return response.data; - } + const result = await response.json(); + console.log('Received response from n8n rollback:', JSON.stringify(result, null, 2)); - if (!response.data.success) { - console.error('Rollback workflow execution failed:', { - message: response.data.message, - data: response.data - }); - throw new Error(`Rollback workflow execution failed: ${response.data.message || 'Unknown error'}`); - } - - return response.data; + return { + success: true, + results: result + }; } catch (error) { - console.error('Error triggering n8n rollback workflow:', { - error: error instanceof Error ? error.message : String(error), - webhookUrl: this.rollbackWebhookUrl, - errorDetails: error instanceof Error ? error.stack : undefined - }); - throw new Error(`Failed to trigger mission rollback workflow: ${error instanceof Error ? error.message : String(error)}`); + console.error('Error triggering n8n rollback workflow:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }; } } } \ No newline at end of file diff --git a/reacct.json b/reacct.json index c6a581f6..1658a755 100644 --- a/reacct.json +++ b/reacct.json @@ -3,7 +3,8 @@ "nodes": [ { "parameters": { - "jsCode": "const missionData = $input.item.json;\nconst sanitizeName = (name) => {\n if (!name || typeof name !== 'string') return 'unnamed-mission';\n const timestamp = new Date().getTime();\n const uniqueSuffix = `-${timestamp}`;\n const result = name.toLowerCase()\n .split('')\n .map(c => {\n if (c >= 'a' && c <= 'z') return c;\n if (c >= '0' && c <= '9') return c;\n if (c === ' ' || c === '-') return c;\n return '';\n })\n .join('')\n .split(' ')\n .filter(Boolean)\n .join('-');\n return result + uniqueSuffix;\n};\nconst formatDate = (date) => {\n if (!date) return '';\n const d = new Date(date);\n return d.toISOString().split('T')[0];\n};\nconst missionName = missionData?.missionOriginal?.body?.name || missionData?.body?.name || missionData?.name || 'Unnamed Mission';\n\n// Prepare file data for MinIO\nconst prepareFileData = (file) => {\n if (!file) {\n // Return default logo if no file provided\n return {\n data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',\n name: 'default-logo.png',\n type: 'image/png'\n };\n }\n \n // Handle different file formats\n if (typeof file === 'string') {\n return {\n data: file,\n name: 'logo.png',\n type: 'image/png'\n };\n }\n \n if (typeof file === 'object') {\n // Handle binary data\n if (file.data && typeof file.data === 'object' && file.data.data) {\n return {\n data: file.data.data,\n name: file.name || 'logo.png',\n type: file.type || 'image/png'\n };\n }\n \n // Handle base64 data\n if (file.data && typeof file.data === 'string') {\n return {\n data: file.data,\n name: file.name || 'logo.png',\n type: file.type || 'image/png'\n };\n }\n \n // Handle direct object\n return {\n data: file,\n name: file.name || 'logo.png',\n type: file.type || 'image/png'\n };\n }\n \n return null;\n};\n\nconst output = {\n missionOriginal: missionData,\n missionProcessed: {\n name: missionName,\n sanitizedName: sanitizeName(missionName),\n intention: missionData?.missionOriginal?.body?.intention || missionData?.body?.intention || missionData?.intention || '',\n description: missionData?.missionOriginal?.body?.intention || missionData?.body?.intention || missionData?.intention || 'Mission documentation',\n startDate: formatDate(new Date()),\n endDate: formatDate(new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)),\n missionType: missionData?.missionOriginal?.body?.missionType || missionData?.body?.missionType || missionData?.missionType || 'default',\n guardians: missionData?.missionOriginal?.body?.guardians || missionData?.body?.guardians || missionData?.guardians || {},\n volunteers: missionData?.missionOriginal?.body?.volunteers || missionData?.body?.volunteers || missionData?.volunteers || [],\n profils: missionData?.missionOriginal?.body?.profils || missionData?.body?.profils || missionData?.profils || [],\n services: missionData?.missionOriginal?.body?.services || missionData?.body?.services || missionData?.services || [],\n clientId: (missionData?.missionOriginal?.body?.missionType === 'interne' || missionData?.body?.missionType === 'interne' || missionData?.missionType === 'interne') ? 1 : 2,\n rocketChatUsernames: [],\n logo: prepareFileData(missionData?.logo),\n attachments: Array.isArray(missionData?.attachments) ? missionData.attachments.map(prepareFileData).filter(Boolean) : []\n },\n config: {\n GITEA_API_URL: \"https://gite.slm-lab.net/api/v1\",\n GITEA_API_TOKEN: \"310645d564cbf752be1fe3b42582a3d5f5d0bddd\",\n GITEA_OWNER: \"alma\",\n LEANTIME_API_URL: \"https://agilite.slm-lab.net\",\n LEANTIME_API_TOKEN: \"lt_lsdShQdoYHaPUWuL07XZR1Rf3GeySsIs_UDlll3VJPk5EwAuILpMC4BwzJ9MZFRrb\",\n ROCKETCHAT_API_URL: \"https://parole.slm-lab.net/\",\n ROCKETCHAT_AUTH_TOKEN: \"w91TYgkH-Z67Oz72usYdkW5TZLLRwnre7qyAhp7aHJB\",\n ROCKETCHAT_USER_ID: \"Tpuww59PJKsrGNQJB\",\n OUTLINE_API_URL: \"https://chapitre.slm-lab.net/api\",\n OUTLINE_API_TOKEN: \"ol_api_tlLlANBfcoJ4l7zA8GOcpduAeL6QyBTcYvEnlN\",\n MISSION_API_URL: \"https://hub.slm-lab.net\",\n N8N_API_KEY: \"LwgeE1ntADD20OuWC88S3pR0EaO7FtO4\",\n KEYCLOAK_BASE_URL: \"https://connect.slm-lab.net\",\n KEYCLOAK_REALM: \"cercle\",\n KEYCLOAK_CLIENT_ID: \"lab\",\n KEYCLOAK_CLIENT_SECRET: \"LwgeE1ntADD20OuWC88S3P0EaO7FtO4\",\n MINIO_API_URL: \"https://dome-api.slm-lab.net\",\n MINIO_SECRET_KEY: \"gbdrqJsXyU4IFxsfz9xdrnQeMRy2eZHeqQRrAeBR\"\n }\n};\n\nconst guardians = missionData?.missionOriginal?.body?.guardians || missionData?.body?.guardians || missionData?.guardians || {};\nif (guardians) {\n for (const role in guardians) {\n const user = guardians[role];\n if (user) output.missionProcessed.rocketChatUsernames.push(user);\n }\n}\nconst volunteers = missionData?.missionOriginal?.body?.volunteers || missionData?.body?.volunteers || missionData?.volunteers || [];\nif (Array.isArray(volunteers)) {\n output.missionProcessed.rocketChatUsernames.push(...volunteers);\n}\noutput.missionProcessed.rocketChatUsernames = [...new Set(output.missionProcessed.rocketChatUsernames)];\n\nreturn output;" + "jsCode": "const missionData = $input.item.json;\nconst sanitizeName = (name) => {\n if (!name || typeof name !== 'string') return 'unnamed-mission';\n const timestamp = new Date().getTime();\n const uniqueSuffix = `-${timestamp}`;\n const result = name.toLowerCase()\n .split('')\n .map(c => {\n if (c >= 'a' && c <= 'z') return c;\n if (c >= '0' && c <= '9') return c;\n if (c === ' ' || c === '-') return c;\n return '';\n })\n .join('')\n .split(' ')\n .filter(Boolean)\n .join('-');\n return result + uniqueSuffix;\n};\nconst formatDate = (date) => {\n if (!date) return '';\n const d = new Date(date);\n return d.toISOString().split('T')[0];\n};\nconst missionName = missionData?.missionOriginal?.body?.name || missionData?.body?.name || missionData?.name || 'Unnamed Mission';\n\n// Prepare file data for MinIO\nconst prepareFileData = (file) => {\n if (!file) {\n // Return default logo if no file provided\n return {\n data: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',\n name: 'default-logo.png',\n type: 'image/png'\n };\n }\n \n // Handle different file formats\n if (typeof file === 'string') {\n return {\n data: file,\n name: 'logo.png',\n type: 'image/png'\n };\n }\n \n if (typeof file === 'object') {\n // Handle binary data\n if (file.data && typeof file.data === 'object' && file.data.data) {\n return {\n data: file.data.data,\n name: file.name || 'logo.png',\n type: file.type || 'image/png'\n };\n }\n \n // Handle base64 data\n if (file.data && typeof file.data === 'string') {\n return {\n data: file.data,\n name: file.name || 'logo.png',\n type: file.type || 'image/png'\n };\n }\n \n // Handle direct object\n return {\n data: file,\n name: file.name || 'logo.png',\ + type: file.type || 'image/png'\n };\n }\n \n return null;\n};\n\nconst output = {\n missionOriginal: missionData,\n missionProcessed: {\n name: missionName,\n sanitizedName: sanitizeName(missionName),\n intention: missionData?.missionOriginal?.body?.intention || missionData?.body?.intention || missionData?.intention || '',\n description: missionData?.missionOriginal?.body?.intention || missionData?.body?.intention || missionData?.intention || 'Mission documentation',\n startDate: formatDate(new Date()),\n endDate: formatDate(new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)),\n missionType: missionData?.missionOriginal?.body?.missionType || missionData?.body?.missionType || missionData?.missionType || 'default',\n guardians: missionData?.missionOriginal?.body?.guardians || missionData?.body?.guardians || missionData?.guardians || {},\n volunteers: missionData?.missionOriginal?.body?.volunteers || missionData?.body?.volunteers || missionData?.volunteers || [],\n profils: missionData?.missionOriginal?.body?.profils || missionData?.body?.profils || missionData?.profils || [],\n services: missionData?.missionOriginal?.body?.services || missionData?.body?.services || missionData?.services || [],\n clientId: (missionData?.missionOriginal?.body?.missionType === 'interne' || missionData?.body?.missionType === 'interne' || missionData?.missionType === 'interne') ? 1 : 2,\n rocketChatUsernames: [],\n logo: prepareFileData(missionData?.logo),\n attachments: Array.isArray(missionData?.attachments) ? missionData.attachments.map(prepareFileData).filter(Boolean) : []\n },\n config: {\n GITEA_API_URL: \"https://gite.slm-lab.net/api/v1\",\n GITEA_API_TOKEN: \"310645d564cbf752be1fe3b42582a3d5f5d0bddd\",\n GITEA_OWNER: \"alma\",\n LEANTIME_API_URL: \"https://agilite.slm-lab.net\",\n LEANTIME_API_TOKEN: \"lt_lsdShQdoYHaPUWuL07XZR1Rf3GeySsIs_UDlll3VJPk5EwAuILpMC4BwzJ9MZFRrb\",\n ROCKETCHAT_API_URL: \"https://parole.slm-lab.net/\",\n ROCKETCHAT_AUTH_TOKEN: \"w91TYgkH-Z67Oz72usYdkW5TZLLRwnre7qyAhp7aHJB\",\n ROCKETCHAT_USER_ID: \"Tpuww59PJKsrGNQJB\",\n OUTLINE_API_URL: \"https://chapitre.slm-lab.net/api\",\n OUTLINE_API_TOKEN: \"ol_api_tlLlANBfcoJ4l7zA8GOcpduAeL6QyBTcYvEnlN\",\n MISSION_API_URL: \"https://hub.slm-lab.net\",\n N8N_API_KEY: \"LwgeE1ntADD20OuWC88S3pR0EaO7FtO4\",\n KEYCLOAK_BASE_URL: \"https://connect.slm-lab.net\",\n KEYCLOAK_REALM: \"cercle\",\n KEYCLOAK_CLIENT_ID: \"lab\",\n KEYCLOAK_CLIENT_SECRET: \"LwgeE1ntADD20OuWC88S3P0EaO7FtO4\",\n MINIO_API_URL: \"https://dome-api.slm-lab.net\",\n MINIO_SECRET_KEY: \"gbdrqJsXyU4IFxsfz9xdrnQeMRy2eZHeqQRrAeBR\"\n }\n};\n\nconst guardians = missionData?.missionOriginal?.body?.guardians || missionData?.body?.guardians || missionData?.guardians || {};\nif (guardians) {\n for (const role in guardians) {\n const user = guardians[role];\n if (user) output.missionProcessed.rocketChatUsernames.push(user);\n }\n}\nconst volunteers = missionData?.missionOriginal?.body?.volunteers || missionData?.body?.volunteers || missionData?.volunteers || [];\nif (Array.isArray(volunteers)) {\n output.missionProcessed.rocketChatUsernames.push(...volunteers);\n}\noutput.missionProcessed.rocketChatUsernames = [...new Set(output.missionProcessed.rocketChatUsernames)];\n\nreturn output;" }, "name": "Process Mission Data", "type": "n8n-nodes-base.code", @@ -544,7 +545,8 @@ "httpMethod": "POST", "path": "mission-created", "options": { - "responseData": "lastNodeJson" + "responseData": "allEntries", + "responseContentType": "application/json" } }, "name": "Mission Created Webhook",