From 25157439c3b34fa3d72a11f0e18502c4fd1a301d Mon Sep 17 00:00:00 2001 From: alma Date: Fri, 23 May 2025 10:24:40 +0200 Subject: [PATCH] W n8n --- .DS_Store | Bin 10244 -> 10244 bytes My_workflow_38.json | 933 -------------------------------------- My_workflow_41.json | 275 +++++++++++ app/api/missions/route.ts | 371 ++++++--------- 4 files changed, 425 insertions(+), 1154 deletions(-) delete mode 100644 My_workflow_38.json create mode 100644 My_workflow_41.json diff --git a/.DS_Store b/.DS_Store index 21e5b2010925334de7b0f14738e18834d44e7fd9..b6b5f3cc4941fef7176d16e954365851ce39aefd 100644 GIT binary patch delta 46 zcmV+}0MY-1P=rvBPXQ*eP`eKSCX);hQ?scM^a7K;6zsDF76J;ffW`r{2O#h($ delta 441 zcmZn(XbG6$F8U^hRb&SV~eU_nuaWQH7uRE9)`JRq6E5I^~gkOmvWdIkojbDKj1 z{xhkHGx#!8GQ=~KGvqTAF=R8O0oCUNdGQQ}3`Puk3`sz}*^?)TNK8H;z@o);?mrkX zfPgSQtspIW5Q~V?V#HtwwxvJ>XiFSXT8tSiU|N`%;wF0te-D&n@B_L&k3j+GZxEfv zkO}lvF2hTph#Am777WG=1`OsveB1{$Vm=oW0CRh5fB*mh diff --git a/My_workflow_38.json b/My_workflow_38.json deleted file mode 100644 index 3b97e08c..00000000 --- a/My_workflow_38.json +++ /dev/null @@ -1,933 +0,0 @@ -{ - "name": "My workflow 38", - "nodes": [ - { - "parameters": { - "jsCode": "const missionData = $input.item.json;\nconst binaryData = $input.item.binary;\n\n// Add detailed logging\nconsole.log('Process Mission Data - Input:', {\n hasInput: !!missionData,\n hasBinary: !!binaryData,\n hasBody: !!missionData?.body,\n hasLogo: !!missionData?.logo,\n hasAttachments: Array.isArray(missionData?.attachments),\n attachmentsCount: missionData?.attachments?.length || 0,\n logoDataType: typeof missionData?.logo,\n logoData: missionData?.logo ? 'present' : 'missing',\n contentType: missionData?.missionOriginal?.headers?.['content-type'] || 'unknown',\n binaryKeys: binaryData ? Object.keys(binaryData) : [],\n binaryDataTypes: binaryData ? Object.keys(binaryData).map(key => typeof binaryData[key]?.data) : []\n});\n\n// Handle raw file input\nif (missionData?.missionOriginal?.headers?.['content-type']?.startsWith('image/')) {\n console.log('Detected raw image file input');\n \n // Get binary data from the first available key\n const binaryKey = Object.keys(binaryData || {})[0];\n const rawData = binaryKey ? binaryData[binaryKey]?.data : null;\n \n if (!rawData) {\n console.error('No binary data found in raw file input');\n throw new Error('No binary data found in raw file input');\n }\n \n // Ensure rawData is a Buffer\n const buffer = Buffer.isBuffer(rawData) ? rawData : Buffer.from(rawData);\n \n // Convert raw data to base64\n const base64Data = buffer.toString('base64');\n const mimeType = missionData.missionOriginal.headers['content-type'];\n \n // Create mission data structure\n return {\n missionOriginal: missionData,\n missionProcessed: {\n name: \"Unnamed Mission\",\n sanitizedName: `unnamed-mission-${Date.now()}`,\n intention: \"\",\n description: \"Mission documentation\",\n startDate: new Date().toISOString().split('T')[0],\n endDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],\n missionType: \"default\",\n guardians: {},\n volunteers: [],\n profils: [],\n services: [],\n clientId: 2,\n rocketChatUsernames: [],\n logo: {\n data: `data:${mimeType};base64,${base64Data}`,\n name: \"logo.png\",\n type: mimeType\n },\n attachments: []\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_ACCESS_KEY: \"4aBT4CMb7JIMMyUtp4Pl\",\n MINIO_SECRET_KEY: \"HGn39XhCIlqOjmDVzRK9MED2Fci2rYvDDgbLFElg\"\n },\n binary: {\n data: buffer\n }\n };\n}\n\n// Continue with existing JSON processing\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_ACCESS_KEY: \"4aBT4CMb7JIMMyUtp4Pl\",\n MINIO_SECRET_KEY: \"HGn39XhCIlqOjmDVzRK9MED2Fci2rYvDDgbLFElg\"\n }\n};\n\n// Add binary data to output if available\nif (binaryData) {\n const binaryKey = Object.keys(binaryData)[0];\n if (binaryKey && binaryData[binaryKey]?.data) {\n // Ensure the data is a Buffer\n const data = binaryData[binaryKey].data;\n output.binary = {\n data: Buffer.isBuffer(data) ? data : Buffer.from(data)\n };\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\n// Ensure binary data is always available\nif (!output.binary || !output.binary.data) {\n console.log('No binary data found, using default PNG');\n output.binary = {\n data: Buffer.from(\"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=\", 'base64')\n };\n}\n\nreturn output;" - }, - "name": "Process Mission Data", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - -1040, - 600 - ], - "id": "4d14c453-908d-4294-8af8-7ae4786e99dd" - }, - { - "parameters": { - "functionCode": "const input = $input.item.json;\nconst binaryData = $input.item.binary;\n\n// Detailed input tracing\nconsole.log(\"Decode Logo Data - Detailed Input Trace:\", {\n // Input structure\n hasInput: !!input,\n inputType: typeof input,\n inputKeys: input ? Object.keys(input) : [],\n \n // Binary data\n hasBinary: !!binaryData,\n binaryType: typeof binaryData,\n binaryKeys: binaryData ? Object.keys(binaryData) : [],\n \n // Mission data\n hasMissionProcessed: !!input?.missionProcessed,\n missionProcessedKeys: input?.missionProcessed ? Object.keys(input.missionProcessed) : [],\n \n // Logo data\n hasLogo: !!input?.missionProcessed?.logo,\n logoType: typeof input?.missionProcessed?.logo,\n logoKeys: input?.missionProcessed?.logo ? Object.keys(input.missionProcessed.logo) : [],\n \n // Headers\n hasHeaders: !!input?.missionOriginal?.headers,\n contentType: input?.missionOriginal?.headers?.['content-type'],\n contentDisposition: input?.missionOriginal?.headers?.['content-disposition']\n});\n\n// Default transparent PNG base64 (1x1 pixel)\nconst DEFAULT_PNG = \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=\";\n\n// Helper function to create a valid buffer with detailed logging\nconst createValidBuffer = (data, source) => {\n console.log(`Creating buffer from ${source}:`, {\n dataType: typeof data,\n isBuffer: Buffer.isBuffer(data),\n isString: typeof data === 'string',\n isObject: typeof data === 'object',\n hasData: data?.data ? 'yes' : 'no'\n });\n \n try {\n if (!data) {\n console.log(`${source}: No data provided`);\n return null;\n }\n \n // If it's already a buffer, return it\n if (Buffer.isBuffer(data)) {\n console.log(`${source}: Data is already a buffer`);\n return data;\n }\n \n // If it's a string, try to create a buffer\n if (typeof data === 'string') {\n // Check if it's base64\n if (data.includes(',')) {\n console.log(`${source}: Converting base64 string to buffer`);\n const base64Data = data.split(',')[1];\n return Buffer.from(base64Data, 'base64');\n }\n // Try as raw string\n console.log(`${source}: Converting raw string to buffer`);\n return Buffer.from(data);\n }\n \n // If it's an object with data property\n if (data && typeof data === 'object' && data.data) {\n console.log(`${source}: Converting object with data property`);\n return createValidBuffer(data.data, `${source}.data`);\n }\n \n console.log(`${source}: Could not create buffer from data`);\n return null;\n } catch (e) {\n console.error(`Error creating buffer from ${source}:`, e);\n return null;\n }\n};\n\n// Try to get binary data with detailed logging\nconst getBinaryData = () => {\n // First try: Check raw binary data\n if (binaryData) {\n console.log('Checking raw binary data...');\n const binaryKey = Object.keys(binaryData)[0];\n if (binaryKey && binaryData[binaryKey]?.data) {\n const buffer = createValidBuffer(binaryData[binaryKey].data, 'raw binary');\n if (buffer) {\n console.log('Successfully created buffer from raw binary data');\n return {\n buffer,\n fileName: input.missionOriginal?.headers?.['content-disposition']?.split('filename=')[1] || 'logo.png',\n mimeType: input.missionOriginal?.headers?.['content-type'] || 'image/png'\n };\n }\n }\n }\n \n // Second try: Check mission processed logo\n console.log('Checking mission processed logo...');\n const logo = input?.missionProcessed?.logo || input?.body?.logo || input?.logo;\n if (logo?.data) {\n const buffer = createValidBuffer(logo.data, 'mission logo');\n if (buffer) {\n console.log('Successfully created buffer from mission logo');\n return {\n buffer,\n fileName: logo.name || 'logo.png',\n mimeType: logo.type || 'image/png'\n };\n }\n }\n \n // Third try: Check if input is raw binary\n console.log('Checking if input is raw binary...');\n if (input && typeof input === 'object' && !input.missionProcessed) {\n const buffer = createValidBuffer(input, 'raw input');\n if (buffer) {\n console.log('Successfully created buffer from raw input');\n return {\n buffer,\n fileName: 'logo.png',\n mimeType: 'image/png'\n };\n }\n }\n \n // Fallback to default\n console.log('No valid binary data found, using default PNG');\n return {\n buffer: Buffer.from(DEFAULT_PNG, 'base64'),\n fileName: 'default-logo.png',\n mimeType: 'image/png'\n };\n};\n\n// Get the binary data with all fallbacks\nconst { buffer, fileName, mimeType } = getBinaryData();\n\n// Validate buffer before creating output\nif (!buffer || !Buffer.isBuffer(buffer)) {\n console.error('Invalid buffer created, forcing default');\n const defaultBuffer = Buffer.from(DEFAULT_PNG, 'base64');\n \n return {\n json: {\n ...input,\n fileName: 'default-logo.png',\n mimeType: 'image/png',\n sanitizedName: input?.missionProcessed?.sanitizedName || \"unnamed-mission\",\n logoProcessed: true,\n forcedDefault: true\n },\n binary: {\n data: defaultBuffer\n }\n };\n}\n\n// Create output with both json and binary data in the correct structure\nconst output = {\n json: {\n ...input,\n fileName,\n mimeType,\n sanitizedName: input?.missionProcessed?.sanitizedName || \"unnamed-mission\",\n logoProcessed: true,\n bufferSize: buffer.length\n },\n binary: {\n data: buffer\n }\n};\n\n// Log the output for debugging\nconsole.log(\"Decode Logo Data - Final Output:\", {\n fileName: output.json.fileName,\n mimeType: output.json.mimeType,\n bufferSize: output.json.bufferSize,\n hasBinaryData: !!output.binary?.data,\n binaryDataType: typeof output.binary?.data,\n isBuffer: Buffer.isBuffer(output.binary?.data)\n});\n\n// Ensure binary data is always available\nif (!output.binary || !output.binary.data) {\n console.error('Binary data missing in output, forcing default');\n output.binary = {\n data: Buffer.from(DEFAULT_PNG, 'base64')\n };\n}\n\nreturn output;" - }, - "name": "Decode Logo Data", - "type": "n8n-nodes-base.function", - "typeVersion": 1, - "position": [ - -840, - 480 - ], - "id": "328a5788-fd96-42b8-8cf3-ebc3ab51dcdf" - }, - { - "parameters": { - "functionCode": "// Process attachments for S3 upload with improved error handling\nconst input = $input.item.json;\nconst attachments = input?.missionProcessed?.attachments || [];\n\n// Create a default output with a flag indicating if there are attachments\nconst defaultOutput = {\n json: {\n ...input,\n hasAttachments: false,\n attachmentUrls: [] // Initialize empty array for consistency\n }\n};\n\n// If no attachments or invalid input, return default output and skip S3 node\nif (!input || !Array.isArray(attachments) || attachments.length === 0) {\n console.log('No attachments found or invalid input structure');\n return defaultOutput;\n}\n\n// Process the first attachment to check if it's valid\nlet hasValidAttachments = false;\nlet validAttachments = [];\n\n// Check all attachments for validity\ntry {\n for (const attachment of attachments) {\n if (attachment && attachment.data) {\n // Extract pure base64 (remove data:image/... prefix if present)\n let base64Data = attachment.data;\n if (typeof base64Data === 'string' && base64Data.includes(',')) {\n base64Data = base64Data.split(',')[1];\n }\n \n // If we have valid data, mark as valid and include in valid attachments\n if (base64Data && base64Data.trim() !== '') {\n try {\n // Test if it's valid base64 by creating a buffer\n const testBuffer = Buffer.from(base64Data, 'base64');\n if (testBuffer.length > 0) {\n hasValidAttachments = true;\n validAttachments.push({\n data: base64Data,\n name: attachment.name || `attachment-${validAttachments.length}.png`,\n type: attachment.type || 'application/octet-stream'\n });\n }\n } catch (e) {\n console.log(`Skipping invalid attachment: ${e.message}`);\n // Skip this attachment but continue processing others\n }\n }\n }\n }\n} catch (error) {\n // If any error in the loop, log it but don't fail the workflow\n console.error('Error checking attachment validity:', error);\n}\n\n// If no valid attachments after checking, return the default output\nif (!hasValidAttachments || validAttachments.length === 0) {\n console.log('No valid attachments found after validation');\n return defaultOutput;\n}\n\n// At this point, we know we have at least one valid attachment\n// Prepare the output with attachment info\nreturn {\n json: {\n ...input,\n hasAttachments: true,\n attachmentCount: validAttachments.length,\n attachmentData: validAttachments,\n originalAttachmentsCount: attachments.length\n }\n};" - }, - "name": "Check Attachments", - "type": "n8n-nodes-base.function", - "typeVersion": 1, - "position": [ - -840, - 740 - ], - "id": "06dd593a-0905-41fd-a833-dd7a1bfd5cfa" - }, - { - "parameters": { - "conditions": { - "boolean": [ - { - "value1": "={{ $json.hasAttachments }}", - "value2": true - } - ] - } - }, - "name": "IF Has Attachments", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [ - -620, - 740 - ], - "id": "2ae10c1a-3f77-4614-9912-33a5ab16206f" - }, - { - "parameters": { - "functionCode": "// Process single attachment for S3 upload with improved error handling\nconst input = $input.item.json;\n\n// Log for debugging\nconsole.log('Process Attachment Data input:', {\n hasInput: !!input,\n hasAttachmentData: !!input?.attachmentData,\n attachmentDataLength: input?.attachmentData?.length || 0,\n attachmentData: input?.attachmentData ? 'present' : 'missing'\n});\n\n// Create array to hold processed attachments\nconst outputs = [];\n\n// Early return if no attachment data to prevent race conditions\nif (!Array.isArray(input?.attachmentData) || input.attachmentData.length === 0) {\n console.log('No valid attachment data found, returning placeholder');\n return [{ \n json: { \n ...input,\n processingFailed: true,\n reason: 'No valid attachments to process'\n } \n }];\n}\n\n// Process each attachment\ninput.attachmentData.forEach((attachment, index) => {\n try {\n if (!attachment || !attachment.data) {\n console.log(`Skipping attachment ${index}: No data`);\n return;\n }\n \n // Extract pure base64 (remove data:image/... prefix if present)\n let base64Data = attachment.data;\n if (typeof base64Data === 'string' && base64Data.includes(',')) {\n base64Data = base64Data.split(',')[1];\n }\n \n // Skip if no valid base64 data\n if (!base64Data || base64Data.trim() === '') {\n console.log(`Skipping attachment ${index}: Empty data`);\n return;\n }\n \n try {\n // Verify the base64 data is valid\n const buffer = Buffer.from(base64Data, 'base64');\n \n if (buffer.length === 0) {\n console.log(`Skipping attachment ${index}: Empty buffer`);\n return;\n }\n \n // Create output for this attachment\n outputs.push({\n json: {\n ...input,\n fileName: attachment.name || `attachment-${index}.${attachment.type?.split('/')[1] || 'bin'}`,\n mimeType: attachment.type || 'application/octet-stream',\n index: index,\n totalAttachments: input.attachmentData.length,\n missionId: input.missionProcessed?.sanitizedName || 'unnamed-mission',\n attachmentProcessed: true\n },\n binary: {\n data: buffer\n }\n });\n \n console.log(`Successfully processed attachment ${index}: ${attachment.name}`);\n } catch (e) {\n console.error(`Failed to create buffer for attachment ${index}:`, e);\n }\n } catch (error) {\n // Skip failed attachments but log the error\n console.error(`Failed to process attachment ${index}:`, error);\n }\n});\n\n// Return processed attachments or a placeholder if none processed\nif (outputs.length > 0) {\n console.log(`Successfully processed ${outputs.length} attachments`);\n return outputs;\n} else {\n console.log('No attachments were successfully processed');\n return [{ \n json: { \n ...input,\n processingFailed: true,\n reason: 'All attachments failed processing'\n } \n }];\n}" - }, - "name": "Process Attachment Data", - "type": "n8n-nodes-base.function", - "typeVersion": 1, - "position": [ - -440, - 680 - ], - "id": "b6010989-f0d7-4ce3-8ac9-fd87a99d06af" - }, - { - "parameters": { - "operation": "upload", - "bucketName": "=missions", - "fileName": "={{$input.item.json.sanitizedName}}/{{$input.item.json.fileName}}", - "additionalFields": { - "acl": "public-read" - } - }, - "id": "d2fa51d1-dcc7-463d-a1b7-b75911da28d6", - "name": "S3 Upload Logo", - "type": "n8n-nodes-base.s3", - "typeVersion": 1, - "position": [ - -500, - 480 - ], - "credentials": { - "s3": { - "id": "xvSkHfsTBJxzopIj", - "name": "S3 account 2" - } - }, - "continueOnFail": true - }, - { - "parameters": { - "functionCode": "// Debug node to ensure binary data is properly structured\nconst input = $input.item;\n\n// Log the full input structure\nconsole.log('Debug - Input structure:', {\n hasJson: !!input.json,\n hasBinary: !!input.binary,\n hasBinaryData: !!input.binary?.data,\n binaryDataType: typeof input.binary?.data,\n isBuffer: Buffer.isBuffer(input.binary?.data),\n jsonKeys: input.json ? Object.keys(input.json) : [],\n binaryKeys: input.binary ? Object.keys(input.binary) : []\n});\n\n// Ensure binary data is properly structured\nif (!input.binary?.data) {\n console.error('No binary data found in input');\n throw new Error('No binary data found in input');\n}\n\n// Return the input unchanged\nreturn input;" - }, - "name": "Debug Binary Data", - "type": "n8n-nodes-base.function", - "typeVersion": 1, - "position": [ - -720, - 480 - ], - "id": "bc8f77df-baad-43db-a0b7-992401913aa5" - }, - { - "parameters": { - "functionCode": "// Generate empty attachment result when there are no attachments\nconst input = $input.item.json;\n\n// Return input with empty attachment urls\nreturn {\n json: {\n ...input,\n attachmentUrls: [],\n noAttachments: true\n }\n};" - }, - "name": "Empty Attachment Result", - "type": "n8n-nodes-base.function", - "typeVersion": 1, - "position": [ - -320, - 880 - ], - "id": "de904a5f-06c7-41ef-be2d-e210f3cafc68" - }, - { - "parameters": { - "functionCode": "// Combine file upload results with improved error handling\nconst input = $input.item.json;\n\n// Debug log\nconsole.log('Process Upload Results received input');\n\n// Defensive access to logo result\nlet logoResult;\ntry {\n logoResult = $node['S3 Upload Logo']?.json || {};\n console.log('Logo upload result accessed successfully');\n} catch (e) {\n console.error('Error accessing logo upload result:', e);\n logoResult = {};\n}\n\n// Check if we have a valid logo URL\nlet logoUrl = '';\nif (logoResult && logoResult.Location) {\n logoUrl = logoResult.Location;\n console.log('Found logo URL:', logoUrl);\n} else if (logoResult && logoResult.error) {\n console.error('Logo upload error:', logoResult.error);\n} else {\n console.log('No logo URL found in upload result');\n}\n\n// Initialize empty attachment URLs array\nlet attachmentUrls = [];\n\n// Handle attachments based on whether they exist\nif (input.hasAttachments === true) {\n try {\n // Try to get results from S3 Upload Attachments node\n const attachmentsResults = $node['S3 Upload Attachments']?.json;\n console.log('Attachment results accessed successfully');\n \n if (attachmentsResults) {\n if (Array.isArray(attachmentsResults)) {\n // Filter out any undefined or null values\n attachmentUrls = attachmentsResults\n .filter(result => result && result.Location)\n .map(result => result.Location);\n console.log(`Processed ${attachmentUrls.length} attachment URLs from array`);\n } else if (attachmentsResults.Location) {\n // Single attachment case\n attachmentUrls = [attachmentsResults.Location];\n console.log('Processed single attachment URL');\n }\n } else {\n console.log('No attachment results found');\n }\n } catch (error) {\n console.error('Error processing attachment results:', error.message);\n }\n} else {\n // If no attachments, use empty array\n console.log('No attachments to process');\n attachmentUrls = input.attachmentUrls || [];\n}\n\n// Create result object with necessary data for downstream nodes\nconst result = {\n ...input,\n logoUrl: logoUrl,\n attachmentUrls: attachmentUrls,\n publicUrl: logoUrl, // For backward compatibility\n logoProcessed: !!logoUrl,\n attachmentsProcessed: attachmentUrls.length > 0,\n processingTimestamp: new Date().toISOString()\n};\n\n// Ensure critical properties exist\nif (!result.missionProcessed) {\n console.log('Restoring missionProcessed from input');\n result.missionProcessed = input.missionProcessed || {};\n}\n\nif (!result.missionOriginal) {\n console.log('Restoring missionOriginal from input');\n result.missionOriginal = input.missionOriginal || {};\n}\n\nif (!result.config) {\n console.log('Restoring config from input');\n result.config = input.config || {};\n}\n\nconsole.log('Process Upload Results completed');\nreturn { json: result };" - }, - "name": "Process Upload Results", - "type": "n8n-nodes-base.function", - "typeVersion": 1, - "position": [ - -60, - 780 - ], - "id": "e8470228-a671-43c0-a5d6-0ed097832b0e" - }, - { - "parameters": { - "method": "POST", - "url": "={{ $node['Process Mission Data'].json.config.KEYCLOAK_BASE_URL + '/realms/' + $node['Process Mission Data'].json.config.KEYCLOAK_REALM + '/protocol/openid-connect/token' }}", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Content-Type", - "value": "application/x-www-form-urlencoded" - } - ] - }, - "sendBody": true, - "bodyParameters": { - "parameters": [ - { - "name": "grant_type", - "value": "client_credentials" - }, - { - "name": "client_id", - "value": "={{ $node['Process Mission Data'].json.config.KEYCLOAK_CLIENT_ID }}" - }, - { - "name": "client_secret", - "value": "LwgeE1ntADD20OuWC88S3pR0EaO7FtO4" - } - ] - }, - "options": { - "allowUnauthorizedCerts": true, - "response": { - "response": { - "fullResponse": true - } - }, - "timeout": 30000 - } - }, - "name": "Get Keycloak Token", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 3, - "position": [ - -80, - 520 - ], - "id": "5a4cb717-cd3a-4c1a-9d49-ca660c724490", - "continueOnFail": true - }, - { - "parameters": { - "functionCode": "const input = $input.item.json;\n\n// Log full input for debugging\nconsole.log('Keycloak response received:', JSON.stringify(input));\n\n// Handle potential errors from Keycloak\nif (input.error || (input.statusCode >= 400 && input.statusCode <= 599)) {\n console.error('Keycloak error detected. Status:', input.statusCode);\n console.error('Error details:', JSON.stringify(input.error || input.body || input));\n \n // If there's a specific error message in the response body, extract it\n let errorMessage = 'Unknown error from Keycloak';\n let errorDetails = '';\n \n try {\n if (input.error?.message) {\n // Try to parse the error message if it's JSON\n if (input.error.message.includes('{\"error\"')) {\n const errorJson = JSON.parse(input.error.message.substring(input.error.message.indexOf('{')));\n errorMessage = errorJson.error || errorMessage;\n errorDetails = errorJson.error_description || '';\n } else {\n errorMessage = input.error.message;\n }\n } else if (typeof input.body === 'object' && input.body.error) {\n errorMessage = input.body.error;\n errorDetails = input.body.error_description || '';\n }\n } catch (e) {\n console.error('Error parsing Keycloak error:', e);\n }\n \n // Return a default object to allow workflow to continue\n return { json: { \n ...input, // Preserve all original data\n access_token: 'ERROR_FETCHING_TOKEN',\n error: errorMessage,\n errorDetails: errorDetails,\n original_error: input.error || input.body || input\n }};\n}\n\n// Extract token from successful response\nconst access_token = input.body?.access_token;\nif (!access_token) {\n console.error('No access token received from Keycloak');\n console.error('Response body:', JSON.stringify(input.body || input));\n \n // Continue with a placeholder token instead of throwing an error\n return { json: { \n ...input, // Preserve all original data\n access_token: 'NO_TOKEN_RECEIVED',\n error: 'Token missing in Keycloak response',\n errorDetails: JSON.stringify(input.body || input)\n }};\n}\n\nconsole.log('Keycloak token received successfully');\n\n// Create new object to ensure ALL input data is preserved and passed through\nconst result = {};\n\n// First, copy ALL properties from the input\nfor (const key in input) {\n if (input.hasOwnProperty(key)) {\n result[key] = input[key];\n }\n}\n\n// Then add/override the access token and ensure critical properties exist\nresult.access_token = access_token;\n\n// Double-check that critical mission data is preserved\nresult.missionProcessed = input.missionProcessed || {};\nresult.missionOriginal = input.missionOriginal || {};\nresult.config = input.config || {};\nresult.logoUrl = input.logoUrl || '';\nresult.publicUrl = input.publicUrl || '';\nresult.attachmentUrls = input.attachmentUrls || [];\n\n// Ensure missionProcessed has all required fields\nif (!result.missionProcessed.sanitizedName) {\n result.missionProcessed.sanitizedName = result.missionProcessed.name ? \n result.missionProcessed.name.toLowerCase().replace(/[^a-z0-9]+/g, '-') : \n `mission-${Date.now()}`;\n}\n\nif (!result.missionProcessed.description) {\n result.missionProcessed.description = 'Mission documentation';\n}\n\nif (!Array.isArray(result.missionProcessed.rocketChatUsernames)) {\n result.missionProcessed.rocketChatUsernames = [];\n}\n\n// Ensure config has all required fields\nif (!result.config.ROCKETCHAT_API_URL) {\n result.config.ROCKETCHAT_API_URL = 'https://parole.slm-lab.net/';\n}\n\nif (!result.config.ROCKETCHAT_AUTH_TOKEN) {\n result.config.ROCKETCHAT_AUTH_TOKEN = 'w91TYgkH-Z67Oz72usYdkW5TZLLRwnre7qyAhp7aHJB';\n}\n\nif (!result.config.ROCKETCHAT_USER_ID) {\n result.config.ROCKETCHAT_USER_ID = 'Tpuww59PJKsrGNQJB';\n}\n\nif (!result.config.OUTLINE_API_URL) {\n result.config.OUTLINE_API_URL = 'https://chapitre.slm-lab.net/api';\n}\n\nif (!result.config.OUTLINE_API_TOKEN) {\n result.config.OUTLINE_API_TOKEN = 'ol_api_tlLlANBfcoJ4l7zA8GOcpduAeL6QyBTcYvEnlN';\n}\n\n// Log the final result for debugging\nconsole.log('Process Token - Final Result:', {\n hasMissionProcessed: !!result.missionProcessed,\n hasConfig: !!result.config,\n sanitizedName: result.missionProcessed.sanitizedName,\n description: result.missionProcessed.description,\n rocketChatUsernames: result.missionProcessed.rocketChatUsernames,\n hasRocketChatConfig: !!(result.config.ROCKETCHAT_API_URL && result.config.ROCKETCHAT_AUTH_TOKEN && result.config.ROCKETCHAT_USER_ID),\n hasOutlineConfig: !!(result.config.OUTLINE_API_URL && result.config.OUTLINE_API_TOKEN)\n});\n\n// Return the enhanced object\nreturn { json: result };" - }, - "name": "Process Token", - "type": "n8n-nodes-base.function", - "typeVersion": 1, - "position": [ - 160, - 680 - ], - "id": "c89254b0-ff16-4d33-8b23-cd084f268afe", - "continueOnFail": true - }, - { - "parameters": { - "functionCode": "// Debug function to check what data is flowing to RocketChat and Documentation\nconst input = $input.item.json;\n\n// Enhanced logging for service nodes\nconsole.log('DEBUG - Detailed Service Data Flow:', {\n // Mission Data\n 'missionProcessed exists': !!input.missionProcessed,\n 'missionProcessed.sanitizedName': input.missionProcessed?.sanitizedName,\n 'missionProcessed.name': input.missionProcessed?.name,\n \n // RocketChat specific\n 'rocketChatUsernames': input.missionProcessed?.rocketChatUsernames,\n 'rocketChatUsernames length': input.missionProcessed?.rocketChatUsernames?.length,\n 'rocketChatUsernames type': typeof input.missionProcessed?.rocketChatUsernames,\n 'ROCKETCHAT_API_URL': input.config?.ROCKETCHAT_API_URL,\n 'ROCKETCHAT_AUTH_TOKEN exists': !!input.config?.ROCKETCHAT_AUTH_TOKEN,\n 'ROCKETCHAT_USER_ID exists': !!input.config?.ROCKETCHAT_USER_ID,\n \n // Documentation specific\n 'OUTLINE_API_URL': input.config?.OUTLINE_API_URL,\n 'OUTLINE_API_TOKEN exists': !!input.config?.OUTLINE_API_TOKEN,\n 'mission description': input.missionProcessed?.description,\n \n // Common data\n 'logoUrl': input.logoUrl,\n 'config exists': !!input.config,\n 'keycloak token': input.access_token?.substring(0, 10) + '...',\n 'LEANTIME_API_URL': input.config?.LEANTIME_API_URL\n});\n\n// Create a copy of the input data to ensure we don't modify the original\nconst output = { ...input };\n\n// Ensure missionProcessed exists and has required fields\noutput.missionProcessed = output.missionProcessed || {};\n\n// Ensure sanitizedName exists\nif (!output.missionProcessed.sanitizedName) {\n output.missionProcessed.sanitizedName = output.missionProcessed.name ? \n output.missionProcessed.name.toLowerCase().replace(/[^a-z0-9]+/g, '-') : \n `mission-${Date.now()}`;\n}\n\n// Ensure description exists\nif (!output.missionProcessed.description) {\n output.missionProcessed.description = 'Mission documentation';\n}\n\n// Ensure rocketChatUsernames is an array\nif (!Array.isArray(output.missionProcessed.rocketChatUsernames)) {\n output.missionProcessed.rocketChatUsernames = [];\n}\n\n// Ensure config exists and has required fields\noutput.config = output.config || {};\n\n// Ensure RocketChat config exists\nif (!output.config.ROCKETCHAT_API_URL) {\n output.config.ROCKETCHAT_API_URL = 'https://parole.slm-lab.net/';\n}\nif (!output.config.ROCKETCHAT_AUTH_TOKEN) {\n output.config.ROCKETCHAT_AUTH_TOKEN = 'w91TYgkH-Z67Oz72usYdkW5TZLLRwnre7qyAhp7aHJB';\n}\nif (!output.config.ROCKETCHAT_USER_ID) {\n output.config.ROCKETCHAT_USER_ID = 'Tpuww59PJKsrGNQJB';\n}\n\n// Ensure Documentation config exists\nif (!output.config.OUTLINE_API_URL) {\n output.config.OUTLINE_API_URL = 'https://chapitre.slm-lab.net/api';\n}\nif (!output.config.OUTLINE_API_TOKEN) {\n output.config.OUTLINE_API_TOKEN = 'ol_api_tlLlANBfcoJ4l7zA8GOcpduAeL6QyBTcYvEnlN';\n}\n\n// Log the final output for debugging\nconsole.log('Debug Service Data - Final Output:', {\n hasMissionProcessed: !!output.missionProcessed,\n hasConfig: !!output.config,\n sanitizedName: output.missionProcessed.sanitizedName,\n description: output.missionProcessed.description,\n rocketChatUsernames: output.missionProcessed.rocketChatUsernames,\n hasRocketChatConfig: !!(output.config.ROCKETCHAT_API_URL && output.config.ROCKETCHAT_AUTH_TOKEN && output.config.ROCKETCHAT_USER_ID),\n hasOutlineConfig: !!(output.config.OUTLINE_API_URL && output.config.OUTLINE_API_TOKEN)\n});\n\n// Return the enhanced object\nreturn { json: output };" - }, - "name": "Debug Service Data", - "type": "n8n-nodes-base.function", - "typeVersion": 1, - "position": [ - 160, - 880 - ], - "id": "70b8b15b-c8d8-4ba0-8016-323c5650352e" - }, - { - "parameters": { - "conditions": { - "string": [ - { - "value1": "={{ Array.isArray($node['Process Mission Data'].json.missionProcessed.services) ? $node['Process Mission Data'].json.missionProcessed.services.includes('Gite') || $node['Process Mission Data'].json.missionProcessed.services.includes('Calcul') : false }}", - "value2": "true" - } - ] - } - }, - "name": "IF Needs Git Repository", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [ - 140, - 500 - ], - "id": "c1f5c793-d0c7-40c6-8ab7-1a482b40603d" - }, - { - "parameters": { - "method": "POST", - "url": "={{ $node['Process Mission Data'].json.config.GITEA_API_URL + '/user/repos' }}", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Content-Type", - "value": "application/json" - }, - { - "name": "Authorization", - "value": "={{ 'token ' + $node['Process Mission Data'].json.config.GITEA_API_TOKEN }}" - } - ] - }, - "sendBody": true, - "bodyParameters": { - "parameters": [ - { - "name": "name", - "value": "={{ $node['Process Mission Data'].json.missionProcessed.sanitizedName }}" - }, - { - "name": "private", - "value": "={{ true }}" - }, - { - "name": "auto_init", - "value": "={{ true }}" - }, - { - "name": "avatar_url", - "value": "={{ $node['Process Upload Results'].json.logoUrl }}" - } - ] - }, - "options": { - "allowUnauthorizedCerts": true, - "response": { - "response": { - "fullResponse": true - } - } - } - }, - "name": "Create Git Repository", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 3, - "position": [ - 460, - 460 - ], - "id": "9e0eec9b-554a-40f9-94e1-ed2d597c2aa6", - "continueOnFail": true - }, - { - "parameters": { - "method": "POST", - "url": "={{ $node['Process Mission Data'].json.config.LEANTIME_API_URL + '/api/jsonrpc' }}", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Content-Type", - "value": "application/json" - }, - { - "name": "X-API-Key", - "value": "={{ $node['Process Mission Data'].json.config.LEANTIME_API_TOKEN }}" - } - ] - }, - "sendBody": true, - "bodyParameters": { - "parameters": [ - { - "name": "method", - "value": "leantime.rpc.Projects.Projects.addProject" - }, - { - "name": "jsonrpc", - "value": "2.0" - }, - { - "name": "id", - "value": "1" - }, - { - "name": "params", - "value": "={{ { values: { name: $node['Process Mission Data'].json.missionProcessed.name, clientId: $node['Process Mission Data'].json.missionProcessed.clientId, details: $node['Process Mission Data'].json.missionProcessed.intention, type: 'project', start: $node['Process Mission Data'].json.missionProcessed.startDate, end: $node['Process Mission Data'].json.missionProcessed.endDate, status: 'open', psettings: 'restricted', avatar: $node['Process Upload Results'].json.logoUrl } } }}" - } - ] - }, - "options": { - "allowUnauthorizedCerts": true, - "response": { - "response": { - "fullResponse": true - } - }, - "timeout": 30000 - } - }, - "name": "Create Leantime Project", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 3, - "position": [ - 460, - 620 - ], - "id": "6d5f5486-e9e0-461f-b7f8-fbcbb22154ad", - "continueOnFail": true - }, - { - "parameters": { - "method": "POST", - "url": "={{ $node['Process Mission Data'].json.config.OUTLINE_API_URL + '/collections.create' }}", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Content-Type", - "value": "application/json" - }, - { - "name": "Authorization", - "value": "={{ 'Bearer ' + $node['Process Mission Data'].json.config.OUTLINE_API_TOKEN }}" - } - ] - }, - "sendBody": true, - "bodyParameters": { - "parameters": [ - { - "name": "name", - "value": "={{ $node['Process Mission Data'].json.missionProcessed.sanitizedName }}" - }, - { - "name": "description", - "value": "={{ $node['Process Mission Data'].json.missionProcessed.description || 'Mission documentation' }}" - }, - { - "name": "color", - "value": "#4f46e5" - }, - { - "name": "permission", - "value": "read" - }, - { - "name": "private", - "value": "={{ true }}" - }, - { - "name": "avatarUrl", - "value": "={{ $node['Process Upload Results'].json.logoUrl }}" - } - ] - }, - "options": { - "allowUnauthorizedCerts": true, - "response": { - "response": { - "fullResponse": true - } - }, - "timeout": 30000 - } - }, - "name": "Create Documentation Collection", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 3, - "position": [ - 480, - 940 - ], - "id": "58d40cb1-4231-439f-90c5-bcd7d35a3968", - "continueOnFail": true - }, - { - "parameters": { - "method": "POST", - "url": "={{ $node['Process Mission Data'].json.config.ROCKETCHAT_API_URL + '/api/v1/channels.create' }}", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Content-Type", - "value": "application/json" - }, - { - "name": "X-Auth-Token", - "value": "={{ $node['Process Mission Data'].json.config.ROCKETCHAT_AUTH_TOKEN }}" - }, - { - "name": "X-User-Id", - "value": "={{ $node['Process Mission Data'].json.config.ROCKETCHAT_USER_ID }}" - } - ] - }, - "sendBody": true, - "bodyParameters": { - "parameters": [ - { - "name": "name", - "value": "={{ $node['Process Mission Data'].json.missionProcessed.sanitizedName }}" - }, - { - "name": "members", - "value": "={{ Array.isArray($node['Process Mission Data'].json.missionProcessed.rocketChatUsernames) ? $node['Process Mission Data'].json.missionProcessed.rocketChatUsernames : [] }}" - }, - { - "name": "readOnly", - "value": "false" - }, - { - "name": "avatarUrl", - "value": "={{ $node['Process Upload Results'].json.logoUrl }}" - } - ] - }, - "options": { - "allowUnauthorizedCerts": true, - "response": { - "response": { - "fullResponse": true - } - }, - "timeout": 30000 - } - }, - "name": "Create RocketChat Channel", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 3, - "position": [ - 460, - 760 - ], - "id": "a7791aeb-841d-4d95-b836-16c03b4254ad", - "continueOnFail": true - }, - { - "parameters": { - "functionCode": "// Combine results from all integrations with better error handling\ntry {\n // Defensively get results from each service node\n let gitRepoResult = {};\n let leantimeResult = {};\n let docCollectionResult = {};\n let rocketChatResult = {};\n let uploadResults = {};\n let keycloakToken = {};\n \n try { \n gitRepoResult = $node['Create Git Repository']?.json || {};\n console.log('Git repo node executed successfully'); \n } catch (e) { \n console.log('Git repo node not executed yet, continuing anyway'); \n }\n \n try { \n leantimeResult = $node['Create Leantime Project']?.json || {};\n console.log('Leantime node executed successfully');\n } catch (e) { \n console.log('Leantime node not executed yet, continuing anyway'); \n }\n \n try { \n docCollectionResult = $node['Create Documentation Collection']?.json || {};\n console.log('Documentation node executed successfully');\n } catch (e) { \n console.log('Documentation node not executed yet, continuing anyway'); \n }\n \n try { \n rocketChatResult = $node['Create RocketChat Channel']?.json || {};\n console.log('RocketChat node executed successfully');\n } catch (e) { \n console.log('RocketChat node not executed yet, continuing anyway'); \n }\n \n try { \n uploadResults = $node['Process Upload Results']?.json || {};\n console.log('Upload Results available');\n } catch (e) { \n console.log('Upload Results not available, continuing anyway'); \n }\n \n try { \n keycloakToken = $node['Process Token']?.json || {};\n console.log('Keycloak token available');\n } catch (e) { \n console.log('Keycloak token not available, continuing anyway'); \n }\n \n // Gather information about what executed\n const executedNodes = [];\n if (Object.keys(gitRepoResult).length > 0) executedNodes.push('Git');\n if (Object.keys(leantimeResult).length > 0) executedNodes.push('Leantime');\n if (Object.keys(docCollectionResult).length > 0) executedNodes.push('Documentation');\n if (Object.keys(rocketChatResult).length > 0) executedNodes.push('RocketChat');\n \n console.log(`Executed nodes (${executedNodes.length}): ${executedNodes.join(', ')}`);\n \n // Handle empty results with empty objects to prevent errors\n const results = {\n gitRepo: gitRepoResult.error ? { error: gitRepoResult.error.message || 'Git repository creation failed' } : (gitRepoResult.body || gitRepoResult || {}),\n leantimeProject: leantimeResult.error ? { error: leantimeResult.error.message || 'Leantime project creation failed' } : (leantimeResult.body || leantimeResult || {}),\n docCollection: docCollectionResult.error ? { error: docCollectionResult.error.message || 'Documentation collection creation failed' } : (docCollectionResult.body || docCollectionResult || {}),\n rocketChatChannel: rocketChatResult.error ? { error: rocketChatResult.error.message || 'RocketChat channel creation failed' } : (rocketChatResult.body || rocketChatResult || {}),\n uploadResults: uploadResults || {},\n keycloakToken: keycloakToken || {},\n executedNodes: executedNodes\n };\n \n // Log key details for debugging\n console.log('Git repo HTML URL:', results.gitRepo?.html_url || 'not available');\n console.log('Leantime project ID:', results.leantimeProject?.result?.id || 'not available');\n console.log('Documentation ID:', results.docCollection?.id || 'not available');\n console.log('RocketChat channel ID:', results.rocketChatChannel?.channel?._id || 'not available');\n \n return results;\n} catch (error) {\n console.error('Error in Combine Results:', error);\n // Return minimal object to allow workflow to continue\n return {\n error: `Error combining results: ${error.message}`,\n gitRepo: {},\n leantimeProject: {},\n docCollection: {},\n rocketChatChannel: {}\n };\n}" - }, - "name": "Combine Results", - "type": "n8n-nodes-base.function", - "typeVersion": 1, - "position": [ - 660, - 600 - ], - "id": "fbd19f1e-6247-4691-91eb-19414ff9070c", - "continueOnFail": true - }, - { - "parameters": { - "method": "POST", - "url": "={{ $node['Process Mission Data'].json.config.MISSION_API_URL + '/api/missions' }}", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Content-Type", - "value": "application/json" - }, - { - "name": "x-api-key", - "value": "={{ $node['Process Mission Data'].json.config.N8N_API_KEY }}" - } - ] - }, - "sendBody": true, - "bodyParameters": { - "parameters": [ - { - "name": "name", - "value": "={{ $node['Process Mission Data'].json.missionProcessed.name }}" - }, - { - "name": "niveau", - "value": "={{ $node['Process Mission Data'].json.missionProcessed.niveau || 'default' }}" - }, - { - "name": "intention", - "value": "={{ $node['Process Mission Data'].json.missionProcessed.intention }}" - }, - { - "name": "description", - "value": "={{ $node['Process Mission Data'].json.missionProcessed.description }}" - }, - { - "name": "logo", - "value": "={{ $node['Process Upload Results'].json.logoUrl }}" - }, - { - "name": "attachments", - "value": "={{ $node['Process Upload Results'].json.attachmentUrls }}" - }, - { - "name": "gitRepoUrl", - "value": "={{ $node['Combine Results'].json.gitRepo?.html_url || '' }}" - }, - { - "name": "leantimeProjectId", - "value": "={{ $node['Combine Results'].json.leantimeProject?.result?.id || '' }}" - }, - { - "name": "documentationCollectionId", - "value": "={{ $node['Combine Results'].json.docCollection?.id || '' }}" - }, - { - "name": "rocketchatChannelId", - "value": "={{ $node['Combine Results'].json.rocketChatChannel?.channel?._id || '' }}" - }, - { - "name": "donneurDOrdre", - "value": "={{ $node['Process Mission Data'].json.missionProcessed.donneurDOrdre || 'default' }}" - }, - { - "name": "projection", - "value": "={{ $node['Process Mission Data'].json.missionProcessed.projection || 'default' }}" - }, - { - "name": "missionType", - "value": "={{ $node['Process Mission Data'].json.missionProcessed.missionType || 'default' }}" - } - ] - }, - "options": { - "response": { - "response": { - "fullResponse": true - } - }, - "timeout": 30000 - } - }, - "name": "Save Mission To API", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 3, - "position": [ - 820, - 600 - ], - "id": "d5dfa1f9-604d-4df4-a402-d09245eabdcd" - }, - { - "parameters": { - "jsCode": "// Defensive access to all nodes\nconst missionData = $node['Process Mission Data']?.json?.missionProcessed || {};\nconst integrationResults = $node['Combine Results']?.json || {};\nconst saveMissionResult = $node['Save Mission To API']?.json || {};\nconst errors = [];\nif (saveMissionResult.error) errors.push(`Failed to save mission: ${saveMissionResult.error.message || 'Unknown error'}`);\nif (!integrationResults.gitRepo?.html_url) errors.push('Git repository creation failed');\nif (!integrationResults.leantimeProject?.result?.id) errors.push('Leantime project creation failed');\nif (!integrationResults.rocketChatChannel?.channel?._id) errors.push('RocketChat channel creation failed');\nif (!integrationResults.docCollection?.id) errors.push('Documentation collection creation failed');\nconst output = {\n success: errors.length === 0,\n error: errors.length > 0 ? errors.join('; ') : null,\n errors: errors,\n missionData,\n integrationResults,\n saveMissionResult,\n message: errors.length === 0 ? 'Mission integration complete: All systems updated successfully' : `Mission integration failed: ${errors.join('; ')}`\n};\nreturn output;" - }, - "name": "Process Results", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [ - 1000, - 600 - ], - "id": "1e1cbe4e-2922-488e-b4f2-9e0e905f1da3" - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={{ $node[\"Process Results\"].json }}", - "options": {} - }, - "name": "Respond To Webhook", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1, - "position": [ - 1220, - 600 - ], - "id": "34f13ce7-cd3a-4811-b1ab-9811afa04d83" - }, - { - "parameters": { - "httpMethod": "POST", - "path": "mission-created", - "responseMode": "lastNode", - "responseData": "allEntries", - "options": {} - }, - "name": "Mission Created Webhook", - "type": "n8n-nodes-base.webhook", - "typeVersion": 1, - "position": [ - -1320, - 600 - ], - "webhookId": "mission-created", - "id": "819a0741-25dc-48ee-8771-285c9e4b47ef" - }, - { - "parameters": { - "operation": "upload", - "bucketName": "=missions", - "fileName": "={{$input.item.json.missionId}}/attachments/{{$input.item.json.fileName}}", - "additionalFields": { - "acl": "public-read" - } - }, - "name": "S3 Upload Attachments", - "type": "n8n-nodes-base.s3", - "typeVersion": 1, - "position": [ - -280, - 680 - ], - "id": "b8c06483-20d1-453c-8468-450d2f435d6a", - "credentials": { - "s3": { - "id": "xvSkHfsTBJxzopIj", - "name": "S3 account 2" - } - }, - "continueOnFail": true - } - ], - "pinData": {}, - "connections": { - "Process Mission Data": { - "main": [ - [ - { - "node": "Decode Logo Data", - "type": "main", - "index": 0 - }, - { - "node": "Check Attachments", - "type": "main", - "index": 0 - } - ] - ] - }, - "Decode Logo Data": { - "main": [ - [ - { - "node": "Debug Binary Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "Check Attachments": { - "main": [ - [ - { - "node": "IF Has Attachments", - "type": "main", - "index": 0 - } - ] - ] - }, - "IF Has Attachments": { - "main": [ - [ - { - "node": "Process Attachment Data", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Empty Attachment Result", - "type": "main", - "index": 0 - } - ] - ] - }, - "Process Attachment Data": { - "main": [ - [ - { - "node": "S3 Upload Attachments", - "type": "main", - "index": 0 - } - ] - ] - }, - "S3 Upload Attachments": { - "main": [ - [ - { - "node": "Process Upload Results", - "type": "main", - "index": 0 - } - ] - ] - }, - "Empty Attachment Result": { - "main": [ - [ - { - "node": "Process Upload Results", - "type": "main", - "index": 0 - } - ] - ] - }, - "S3 Upload Logo": { - "main": [ - [ - { - "node": "Process Upload Results", - "type": "main", - "index": 0 - } - ] - ] - }, - "Process Upload Results": { - "main": [ - [ - { - "node": "Get Keycloak Token", - "type": "main", - "index": 0 - } - ] - ] - }, - "Get Keycloak Token": { - "main": [ - [ - { - "node": "Process Token", - "type": "main", - "index": 0 - } - ] - ] - }, - "Process Token": { - "main": [ - [ - { - "node": "Debug Service Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "Debug Service Data": { - "main": [ - [ - { - "node": "IF Needs Git Repository", - "type": "main", - "index": 0 - } - ] - ] - }, - "IF Needs Git Repository": { - "main": [ - [ - { - "node": "Create Git Repository", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Create Leantime Project", - "type": "main", - "index": 0 - } - ] - ] - }, - "Create Git Repository": { - "main": [ - [ - { - "node": "Create Leantime Project", - "type": "main", - "index": 0 - } - ] - ] - }, - "Create Leantime Project": { - "main": [ - [ - { - "node": "Create Documentation Collection", - "type": "main", - "index": 0 - } - ] - ] - }, - "Create Documentation Collection": { - "main": [ - [ - { - "node": "Create RocketChat Channel", - "type": "main", - "index": 0 - } - ] - ] - }, - "Create RocketChat Channel": { - "main": [ - [ - { - "node": "Combine Results", - "type": "main", - "index": 0 - } - ] - ] - }, - "Combine Results": { - "main": [ - [ - { - "node": "Save Mission To API", - "type": "main", - "index": 0 - } - ] - ] - }, - "Save Mission To API": { - "main": [ - [ - { - "node": "Process Results", - "type": "main", - "index": 0 - } - ] - ] - }, - "Process Results": { - "main": [ - [ - { - "node": "Respond To Webhook", - "type": "main", - "index": 0 - } - ] - ] - }, - "Mission Created Webhook": { - "main": [ - [ - { - "node": "Process Mission Data", - "type": "main", - "index": 0 - } - ] - ] - }, - "Debug Binary Data": { - "main": [ - [ - { - "node": "S3 Upload Logo", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "active": false, - "settings": { - "executionOrder": "v1" - }, - "versionId": "832c05ca-cb6b-4cc9-82ad-25bf63a98414", - "meta": { - "templateCredsSetupCompleted": true, - "instanceId": "575d8de48bd511243817deebddae0cc97d73be64c6c4737e5d4e9caddec881d8" - }, - "id": "Pi9vMcgThia5elue", - "tags": [] -} \ No newline at end of file diff --git a/My_workflow_41.json b/My_workflow_41.json new file mode 100644 index 00000000..9d2ef6dd --- /dev/null +++ b/My_workflow_41.json @@ -0,0 +1,275 @@ +{ + "name": "Simplified Mission Creation Workflow", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "mission-created", + "responseMode": "responseNode", + "responseData": "allEntries", + "options": { + "response": { + "response": { + "fullResponse": true + } + }, + "idempotency": { + "enabled": true, + "key": "={{ $json.name }}-{{ $now }}" + } + } + }, + "name": "Mission Created Webhook", + "type": "n8n-nodes-base.webhook", + "typeVersion": 1, + "position": [ + -1320, + 600 + ], + "webhookId": "mission-created", + "id": "01fde23a-f4da-48f6-b1a4-c0ac1210dbb6" + }, + { + "parameters": { + "jsCode": "// Process all mission data in one node\nconst missionData = $input.item.json;\nconst binaryData = $input.item.binary;\n\n// Generate a unique request ID based on mission data\nconst generateRequestId = (data) => {\n const missionName = data?.body?.name || data?.name || '';\n const timestamp = new Date().toISOString().split('T')[0];\n const randomSuffix = Math.random().toString(36).substring(2, 8);\n return `${missionName}-${timestamp}-${randomSuffix}`;\n};\n\n// Check for existing mission with same name and request ID\nconst checkExistingMission = async () => {\n try {\n const requestId = generateRequestId(missionData);\n const response = await fetch(`${missionData?.config?.MISSION_API_URL || 'https://hub.slm-lab.net'}/api/missions?name=${encodeURIComponent(missionData?.body?.name || missionData?.name || '')}&requestId=${requestId}`, {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': missionData?.config?.N8N_API_KEY || 'LwgeE1ntADD20OuWC88S3pR0EaO7FtO4',\n 'x-request-id': requestId,\n 'x-idempotency-key': requestId\n }\n });\n \n if (!response.ok) throw new Error('Failed to check existing mission');\n \n const data = await response.json();\n return {\n exists: Array.isArray(data) && data.length > 0,\n requestId,\n data\n };\n } catch (error) {\n console.error('Error checking existing mission:', error);\n return { exists: false, requestId: null, data: null };\n }\n};\n\n// Check if mission already exists\nconst { exists: missionExists, requestId, data: existingMission } = await checkExistingMission();\nif (missionExists) {\n return {\n missionExists: true,\n message: 'Mission with this name already exists',\n originalData: missionData,\n requestId,\n existingMission\n };\n}\n\n// Add request ID to mission data\nmissionData.requestId = requestId;\n\n// Helper functions\nconst sanitizeName = (name) => {\n if (!name || typeof name !== \"string\") return \"unnamed-mission\";\n return name.toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n};\n\nconst sanitizeDisplayName = (name) => {\n if (!name || typeof name !== \"string\") return \"Unnamed Mission\";\n return name.trim();\n};\n\nconst processFile = (file) => {\n if (!file) return null;\n \n // Handle different file formats\n if (typeof file === \"string\") {\n return {\n data: file,\n name: \"file.png\",\n type: \"image/png\"\n };\n }\n \n if (typeof file === \"object\") {\n if (file.data && typeof file.data === \"object\" && file.data.data) {\n return {\n data: file.data.data,\n name: file.name || \"file.png\",\n type: file.type || \"image/png\"\n };\n }\n \n if (file.data && typeof file.data === \"string\") {\n return {\n data: file.data,\n name: file.name || \"file.png\",\n type: file.type || \"image/png\"\n };\n }\n }\n \n return null;\n};\n\n// Process mission data\nconst missionName = missionData?.body?.name || missionData?.name || \"Unnamed Mission\";\nconst processedData = {\n missionProcessed: {\n name: missionName,\n sanitizedName: sanitizeName(missionName),\n displayName: sanitizeDisplayName(missionName),\n intention: missionData?.body?.intention || missionData?.intention || \"\",\n description: missionData?.body?.intention || missionData?.intention || \"Mission documentation\",\n startDate: new Date().toISOString().split('T')[0],\n endDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],\n missionType: missionData?.body?.missionType || missionData?.missionType || \"default\",\n guardians: missionData?.body?.guardians || missionData?.guardians || {},\n volunteers: missionData?.body?.volunteers || missionData?.volunteers || [],\n profils: missionData?.body?.profils || missionData?.profils || [],\n services: missionData?.body?.services || missionData?.services || [],\n clientId: (missionData?.body?.missionType === \"interne\" || missionData?.missionType === \"interne\") ? 1 : 2,\n rocketChatUsernames: [],\n logo: processFile(missionData?.logo),\n attachments: Array.isArray(missionData?.attachments) ? missionData.attachments.map(processFile).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_ACCESS_KEY: \"4aBT4CMb7JIMMyUtp4Pl\",\n MINIO_SECRET_KEY: \"HGn39XhCIlqOjmDVzRK9MED2Fci2rYvDDgbLFElg\"\n }\n};\n\n// Process guardians and volunteers for RocketChat\nconst guardians = processedData.missionProcessed.guardians;\nif (guardians) {\n for (const role in guardians) {\n const user = guardians[role];\n if (user) processedData.missionProcessed.rocketChatUsernames.push(user);\n }\n}\n\nconst volunteers = processedData.missionProcessed.volunteers;\nif (Array.isArray(volunteers)) {\n processedData.missionProcessed.rocketChatUsernames.push(...volunteers);\n}\n\n// Remove duplicates\nprocessedData.missionProcessed.rocketChatUsernames = [...new Set(processedData.missionProcessed.rocketChatUsernames)];\n\nreturn processedData;" + }, + "name": "Process Mission Data", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -1040, + 600 + ], + "id": "45f11013-1264-444c-8380-cc201cdae7f7" + }, + { + "parameters": { + "jsCode": "// Process and upload all files in one node\nconst input = $input.item.json;\n\n// Helper function to upload file to S3\nconst uploadToS3 = async (file, type) => {\n if (!file || !file.data) return null;\n \n try {\n const response = await fetch(`${input.config.MINIO_API_URL}/upload`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Access-Key': input.config.MINIO_ACCESS_KEY,\n 'X-Secret-Key': input.config.MINIO_SECRET_KEY\n },\n body: JSON.stringify({\n bucket: 'missions',\n path: `${input.missionProcessed.sanitizedName}/${type}/${file.name}`,\n data: file.data,\n contentType: file.type\n })\n });\n \n if (!response.ok) throw new Error('Upload failed');\n \n const result = await response.json();\n return result.url;\n } catch (error) {\n console.error(`Error uploading ${type}:`, error);\n return null;\n }\n};\n\n// Upload logo\nlet logoUrl = null;\nif (input.missionProcessed.logo) {\n logoUrl = await uploadToS3(input.missionProcessed.logo, 'logo');\n}\n\n// Upload attachments\nconst attachmentUrls = [];\nif (Array.isArray(input.missionProcessed.attachments)) {\n for (const attachment of input.missionProcessed.attachments) {\n const url = await uploadToS3(attachment, 'attachments');\n if (url) attachmentUrls.push(url);\n }\n}\n\nreturn {\n json: {\n ...input,\n logoUrl,\n attachmentUrls,\n filesProcessed: true\n }\n};" + }, + "name": "Process Files", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -840, + 600 + ], + "id": "812883bf-385c-4c85-bdf7-69d7a36a2d3a" + }, + { + "parameters": { + "jsCode": "// Integrate with all services in parallel\nconst input = $input.item.json;\n\n// Helper function for service integration\nconst integrateService = async (service, data) => {\n try {\n const response = await fetch(service.url, {\n method: 'POST',\n headers: service.headers,\n body: JSON.stringify(service.body)\n });\n \n if (!response.ok) throw new Error(`Service integration failed: ${response.statusText}`);\n \n return await response.json();\n } catch (error) {\n console.error(`Error in ${service.name}:`, error);\n return { error: error.message };\n }\n};\n\n// Prepare service integrations\nconst services = [];\n\n// Git Repository (if needed)\nif (input.missionProcessed.services.includes('Gite') || input.missionProcessed.services.includes('Calcul')) {\n services.push({\n name: 'Git Repository',\n url: `${input.config.GITEA_API_URL}/user/repos`,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `token ${input.config.GITEA_API_TOKEN}`\n },\n body: {\n name: input.missionProcessed.sanitizedName,\n private: true,\n auto_init: true,\n avatar_url: input.logoUrl\n }\n });\n}\n\n// Leantime Project\nservices.push({\n name: 'Leantime Project',\n url: `${input.config.LEANTIME_API_URL}/api/jsonrpc`,\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': input.config.LEANTIME_API_TOKEN\n },\n body: {\n method: 'leantime.rpc.Projects.Projects.addProject',\n jsonrpc: '2.0',\n id: 1,\n params: {\n values: {\n name: input.missionProcessed.displayName,\n clientId: input.missionProcessed.clientId,\n details: input.missionProcessed.intention,\n type: 'project',\n start: input.missionProcessed.startDate,\n end: input.missionProcessed.endDate,\n status: 'open',\n psettings: 'restricted',\n avatar: input.logoUrl\n }\n }\n }\n});\n\n// Documentation Collection\nservices.push({\n name: 'Documentation Collection',\n url: `${input.config.OUTLINE_API_URL}/collections.create`,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${input.config.OUTLINE_API_TOKEN}`\n },\n body: {\n name: input.missionProcessed.displayName,\n description: input.missionProcessed.description,\n color: '#4f46e5',\n permission: 'read',\n private: true,\n avatarUrl: input.logoUrl\n }\n});\n\n// RocketChat Channel\nservices.push({\n name: 'RocketChat Channel',\n url: `${input.config.ROCKETCHAT_API_URL}/api/v1/channels.create`,\n headers: {\n 'Content-Type': 'application/json',\n 'X-Auth-Token': input.config.ROCKETCHAT_AUTH_TOKEN,\n 'X-User-Id': input.config.ROCKETCHAT_USER_ID\n },\n body: {\n name: input.missionProcessed.displayName,\n members: input.missionProcessed.rocketChatUsernames,\n readOnly: false,\n avatarUrl: input.logoUrl\n }\n});\n\n// Execute all service integrations in parallel\nconst results = await Promise.all(services.map(service => integrateService(service, input)));\n\n// Map results to their respective services\nconst serviceResults = {};\nservices.forEach((service, index) => {\n serviceResults[service.name.toLowerCase().replace(/\\s+/g, '')] = results[index];\n});\n\nreturn {\n json: {\n ...input,\n serviceResults,\n servicesIntegrated: true\n }\n};" + }, + "name": "Integrate Services", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -640, + 600 + ], + "id": "2cd1602d-60ee-4049-a834-4b6c37cbb4cd" + }, + { + "parameters": { + "method": "POST", + "url": "={{ $node['Process Mission Data'].json.config.MISSION_API_URL + '/api/missions' }}", + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Content-Type", + "value": "application/json" + }, + { + "name": "x-api-key", + "value": "={{ $node['Process Mission Data'].json.config.N8N_API_KEY }}" + }, + { + "name": "x-request-id", + "value": "={{ $node['Process Mission Data'].json.requestId }}" + } + ] + }, + "sendBody": true, + "bodyParameters": { + "parameters": [ + { + "name": "name", + "value": "={{ $node['Process Mission Data'].json.missionProcessed.name }}" + }, + { + "name": "niveau", + "value": "={{ $node['Process Mission Data'].json.missionProcessed.niveau || 'default' }}" + }, + { + "name": "intention", + "value": "={{ $node['Process Mission Data'].json.missionProcessed.intention }}" + }, + { + "name": "description", + "value": "={{ $node['Process Mission Data'].json.missionProcessed.description }}" + }, + { + "name": "logo", + "value": "={{ $node['Process Files'].json.logoUrl }}" + }, + { + "name": "attachments", + "value": "={{ $node['Process Files'].json.attachmentUrls }}" + }, + { + "name": "gitRepoUrl", + "value": "={{ $node['Integrate Services'].json.serviceResults.gitrepository?.html_url || '' }}" + }, + { + "name": "leantimeProjectId", + "value": "={{ $node['Integrate Services'].json.serviceResults.leantimeproject?.result?.id || '' }}" + }, + { + "name": "documentationCollectionId", + "value": "={{ $node['Integrate Services'].json.serviceResults.documentationcollection?.id || '' }}" + }, + { + "name": "rocketchatChannelId", + "value": "={{ $node['Integrate Services'].json.serviceResults.rocketchatchannel?.channel?._id || '' }}" + }, + { + "name": "donneurDOrdre", + "value": "={{ $node['Process Mission Data'].json.missionProcessed.donneurDOrdre || 'default' }}" + }, + { + "name": "projection", + "value": "={{ $node['Process Mission Data'].json.missionProcessed.projection || 'default' }}" + }, + { + "name": "missionType", + "value": "={{ $node['Process Mission Data'].json.missionProcessed.missionType || 'default' }}" + } + ] + }, + "options": { + "response": { + "response": { + "fullResponse": true + } + }, + "timeout": 30000 + } + }, + "name": "Save Mission To API", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 3, + "position": [ + -440, + 600 + ], + "id": "cac1aab1-1f4d-4c67-8edf-5d32a39c7ff2" + }, + { + "parameters": { + "jsCode": "// Process final results\nconst input = $input.item.json;\n\n// Handle case when mission already exists\nif (input.missionExists) {\n return {\n success: true,\n message: 'Mission already exists',\n data: input.existingMission,\n requestId: input.requestId,\n isIdempotent: true\n };\n}\n\nconst serviceResults = input.serviceResults || {};\nconst saveMissionResult = input.body || {};\n\n// Check for errors\nconst errors = [];\n\nif (saveMissionResult.error) {\n errors.push(`Failed to save mission: ${saveMissionResult.error.message || 'Unknown error'}`);\n}\n\nif (serviceResults.gitrepository?.error) {\n errors.push(`Git repository creation failed: ${serviceResults.gitrepository.error}`);\n}\n\nif (serviceResults.leantimeproject?.error) {\n errors.push(`Leantime project creation failed: ${serviceResults.leantimeproject.error}`);\n}\n\nif (serviceResults.documentationcollection?.error) {\n errors.push(`Documentation collection creation failed: ${serviceResults.documentationcollection.error}`);\n}\n\nif (serviceResults.rocketchatchannel?.error) {\n errors.push(`RocketChat channel creation failed: ${serviceResults.rocketchatchannel.error}`);\n}\n\n// Prepare response\nconst response = {\n success: errors.length === 0,\n error: errors.length > 0 ? errors.join('; ') : null,\n errors: errors,\n missionData: input.missionProcessed,\n serviceResults: {\n gitRepo: serviceResults.gitrepository,\n leantimeProject: serviceResults.leantimeproject,\n documentation: serviceResults.documentationcollection,\n rocketChat: serviceResults.rocketchatchannel\n },\n saveMissionResult,\n requestId: input.requestId,\n message: errors.length === 0 ? 'Mission integration complete: All systems updated successfully' : `Mission integration failed: ${errors.join('; ')}`\n};\n\nreturn response;" + }, + "name": "Process Results", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -240, + 600 + ], + "id": "428eb077-6f23-447e-804a-803148f0b7e8" + }, + { + "parameters": { + "respondWith": "json", + "responseBody": "={{ $node[\"Process Results\"].json }}", + "options": {} + }, + "name": "Respond To Webhook", + "type": "n8n-nodes-base.respondToWebhook", + "typeVersion": 1, + "position": [ + -40, + 600 + ], + "id": "47b5609f-06d7-4d1a-89a3-55a093b82e31" + } + ], + "connections": { + "Mission Created Webhook": { + "main": [ + [ + { + "node": "Process Mission Data", + "type": "main", + "index": 0 + } + ] + ] + }, + "Process Mission Data": { + "main": [ + [ + { + "node": "Process Files", + "type": "main", + "index": 0 + } + ] + ] + }, + "Process Files": { + "main": [ + [ + { + "node": "Integrate Services", + "type": "main", + "index": 0 + } + ] + ] + }, + "Integrate Services": { + "main": [ + [ + { + "node": "Save Mission To API", + "type": "main", + "index": 0 + } + ] + ] + }, + "Save Mission To API": { + "main": [ + [ + { + "node": "Process Results", + "type": "main", + "index": 0 + } + ] + ] + }, + "Process Results": { + "main": [ + [ + { + "node": "Respond To Webhook", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": true, + "settings": { + "executionOrder": "v1" + }, + "versionId": "678f7cfc-e4f1-4c80-abae-bb23335efe9c", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "575d8de48bd511243817deebddae0cc97d73be64c6c4737e5d4e9caddec881d8" + }, + "id": "jtAwgFTRapzRi0OB", + "tags": [] +} \ No newline at end of file diff --git a/app/api/missions/route.ts b/app/api/missions/route.ts index b353a29b..8fdde8ca 100644 --- a/app/api/missions/route.ts +++ b/app/api/missions/route.ts @@ -2,10 +2,30 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from "@/app/api/auth/options"; import { prisma } from '@/lib/prisma'; -import { getPublicUrl } from '@/lib/s3'; -import { S3_CONFIG } from '@/lib/s3'; import { N8nService } from '@/lib/services/n8n-service'; -import { MissionUser, Prisma } from '@prisma/client'; +import { Prisma } from '@prisma/client'; + +// Types +interface MissionCreateInput { + name: string; + oddScope?: string[]; + niveau: string; + intention: string; + missionType: string; + donneurDOrdre: string; + projection: string; + services?: string[]; + participation?: string; + profils?: string[]; + guardians?: Record; + volunteers?: string[]; + logo?: string | null; + leantimeProjectId?: string; + documentationCollectionId?: string; + rocketchatChannelId?: string; + gitRepoUrl?: string; + penpotProjectId?: string; +} interface MissionUserInput { role: string; @@ -15,41 +35,19 @@ interface MissionUserInput { // Helper function to check authentication async function checkAuth(request: Request) { - // Check for API key in headers first const apiKey = request.headers.get('x-api-key'); - console.log('Received API key from headers:', apiKey); - - // If no API key in headers, try to get it from the request body - let bodyApiKey = null; - if (request.method === 'POST') { - const body = await request.clone().json(); - bodyApiKey = body?.config?.N8N_API_KEY; - console.log('Received API key from body:', bodyApiKey); - } - - const receivedApiKey = apiKey || bodyApiKey; - console.log('Final API key used:', receivedApiKey); - console.log('Expected API key:', process.env.N8N_API_KEY); - console.log('API key match:', receivedApiKey === process.env.N8N_API_KEY); - - if (receivedApiKey === process.env.N8N_API_KEY) { + if (apiKey === process.env.N8N_API_KEY) { return { authorized: true, userId: 'system' }; } - // If no API key, check for session const session = await getServerSession(authOptions); if (!session?.user?.id) { - console.error('Unauthorized access attempt:', { - url: request.url, - method: request.method, - headers: Object.fromEntries(request.headers) - }); return { authorized: false, userId: null }; } return { authorized: true, userId: session.user.id }; } -// GET endpoint to list missions with filters +// GET endpoint to list missions export async function GET(request: Request) { try { const { authorized, userId } = await checkAuth(request); @@ -61,36 +59,27 @@ export async function GET(request: Request) { const limit = Number(searchParams.get('limit') || '10'); const offset = Number(searchParams.get('offset') || '0'); const search = searchParams.get('search'); + const name = searchParams.get('name'); + + const where: Prisma.MissionWhereInput = {}; - // Build query conditions - const where: any = {}; - - // Add search filter if provided if (search) { where.OR = [ { name: { contains: search, mode: 'insensitive' } }, { intention: { contains: search, mode: 'insensitive' } } ]; } - - // Get missions with basic info - const missions = await (prisma as any).mission.findMany({ + + if (name) { + where.name = name; + } + + const missions = await prisma.mission.findMany({ where, skip: offset, take: limit, orderBy: { createdAt: 'desc' }, - select: { - id: true, - name: true, - logo: true, - oddScope: true, - niveau: true, - missionType: true, - projection: true, - participation: true, - services: true, - intention: true, - createdAt: true, + include: { creator: { select: { id: true, @@ -98,9 +87,7 @@ export async function GET(request: Request) { } }, missionUsers: { - select: { - id: true, - role: true, + include: { user: { select: { id: true, @@ -111,18 +98,11 @@ export async function GET(request: Request) { } } }); - - // Get total count - const totalCount = await (prisma as any).mission.count({ where }); - - // Transform logo paths to public URLs - const missionsWithPublicUrls = missions.map((mission: any) => ({ - ...mission, - logo: mission.logo ? `/api/missions/image/${mission.logo}` : null - })); - + + const totalCount = await prisma.mission.count({ where }); + return NextResponse.json({ - missions: missionsWithPublicUrls, + missions, pagination: { total: totalCount, offset, @@ -143,31 +123,17 @@ export async function POST(request: Request) { try { const { authorized, userId } = await checkAuth(request); if (!authorized || !userId) { - console.error('Unauthorized access attempt - no session or user'); return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } - const body = await request.json(); - console.log('Received mission creation request:', JSON.stringify(body, null, 2)); - - const { name, oddScope, niveau, intention, missionType, donneurDOrdre, projection, services, participation, profils, guardians, volunteers } = body; + const requestId = request.headers.get('x-request-id'); + const body = await request.json() as MissionCreateInput; // Validate required fields - const requiredFields = { - name, - niveau, - intention, - missionType, - donneurDOrdre, - projection - }; - - const missingFields = Object.entries(requiredFields) - .filter(([_, value]) => !value) - .map(([key]) => key); + const requiredFields = ['name', 'niveau', 'intention', 'missionType', 'donneurDOrdre', 'projection']; + const missingFields = requiredFields.filter(field => !body[field as keyof MissionCreateInput]); if (missingFields.length > 0) { - console.error('Missing required fields:', missingFields); return NextResponse.json({ error: 'Missing required fields', missingFields @@ -176,45 +142,33 @@ export async function POST(request: Request) { // Check if mission with same name exists const existingMission = await prisma.mission.findFirst({ - where: { name }, - select: { - id: true, - name: true, - rocketChatChannelId: true, - leantimeProjectId: true, - outlineCollectionId: true, - giteaRepositoryUrl: true - } as const + where: { name: body.name } }); if (existingMission) { - console.log('Mission exists, updating integration IDs:', { - missionId: existingMission.id, - name: existingMission.name, - rocketchatChannelId: body.rocketchatChannelId - }); - - // Update the existing mission with new integration IDs - const updateData = { - rocketChatChannelId: body.rocketchatChannelId || existingMission.rocketChatChannelId, - leantimeProjectId: body.leantimeProjectId || existingMission.leantimeProjectId, - outlineCollectionId: body.documentationCollectionId || existingMission.outlineCollectionId, - giteaRepositoryUrl: body.gitRepoUrl || existingMission.giteaRepositoryUrl - } as const; - + // Update existing mission with new integration IDs const updatedMission = await prisma.mission.update({ where: { id: existingMission.id }, - data: updateData as Prisma.MissionUncheckedUpdateInput + data: { + leantimeProjectId: body.leantimeProjectId || null, + outlineCollectionId: body.documentationCollectionId || null, + rocketChatChannelId: body.rocketchatChannelId || null, + giteaRepositoryUrl: body.gitRepoUrl || null, + penpotProjectId: body.penpotProjectId || null + } as Prisma.MissionUpdateInput }); - console.log('Updated existing mission:', JSON.stringify(updatedMission, null, 2)); - return NextResponse.json(updatedMission); + return NextResponse.json({ + message: 'Mission updated successfully', + mission: updatedMission, + isUpdate: true + }); } - // Check if there's already a mission being created with this name + // Check for mission creation in progress const missionInProgress = await prisma.mission.findFirst({ where: { - name, + name: body.name, createdAt: { gte: new Date(Date.now() - 5 * 60 * 1000) // Within last 5 minutes } @@ -222,7 +176,6 @@ export async function POST(request: Request) { }); if (missionInProgress) { - console.log('Mission creation already in progress:', missionInProgress); return NextResponse.json({ error: 'Mission creation already in progress', details: 'Please wait a few minutes and try again', @@ -230,115 +183,31 @@ export async function POST(request: Request) { }, { status: 409 }); } - // Trigger n8n workflow first - const n8nService = new N8nService(); - const n8nData = { - ...body, - creatorId: userId - }; - console.log('Sending data to n8n service:', { - name: n8nData.name, - creatorId: n8nData.creatorId, - oddScope: n8nData.oddScope, - niveau: n8nData.niveau, - intention: n8nData.intention, - missionType: n8nData.missionType, - donneurDOrdre: n8nData.donneurDOrdre, - projection: n8nData.projection, - services: n8nData.services, - participation: n8nData.participation, - profils: n8nData.profils, - fullData: JSON.stringify(n8nData, null, 2) - }); + // Check if this is a request from n8n + const isN8nRequest = request.headers.get('x-api-key') === process.env.N8N_API_KEY; - try { - const workflowResult = await n8nService.triggerMissionCreation(n8nData); - console.log('Received workflow result:', JSON.stringify(workflowResult, null, 2)); + if (!isN8nRequest) { + // Trigger n8n workflow + const n8nService = new N8nService(); + const n8nData = { + ...body, + creatorId: userId, + requestId + }; - if (!workflowResult.success) { - console.error('N8n workflow failed:', workflowResult.error); - return NextResponse.json({ - error: 'Failed to create mission resources', - details: workflowResult.error, - code: 'WORKFLOW_ERROR' - }, { status: 500 }); - } - - // Process workflow results - const results = workflowResult.results || {}; - console.log('Processing workflow results:', JSON.stringify(results, null, 2)); - - // Now create the mission with the logo URL from n8n - console.log('Creating mission in database...'); - const mission = await prisma.mission.create({ - data: { - name, - oddScope: oddScope || ['default'], - niveau, - intention, - missionType, - donneurDOrdre, - projection, - services: Array.isArray(services) ? services.filter(Boolean) : [], - profils: Array.isArray(profils) ? profils.filter(Boolean) : [], - participation: participation || 'default', - creatorId: userId, - logo: results.logoUrl || null, - // Store integration IDs directly in the mission record - leantimeProjectId: results.leantimeProjectId?.toString() || null, - outlineCollectionId: results.outlineCollectionId?.toString() || null, - rocketChatChannelId: results.rocketChatChannelId?.toString() || null, - giteaRepositoryUrl: results.giteaRepositoryUrl?.toString() || null, - penpotProjectId: results.penpotProjectId?.toString() || null - } as Prisma.MissionUncheckedCreateInput - }); - - console.log('Created mission:', JSON.stringify(mission, null, 2)); - - // Add guardians and volunteers - if (guardians || volunteers) { - console.log('Adding guardians and volunteers...'); - const missionUsers: MissionUserInput[] = []; - - // Add guardians - if (guardians) { - Object.entries(guardians).forEach(([role, userId]) => { - if (userId) { - missionUsers.push({ - role, - userId: userId as string, - missionId: mission.id - }); - } - }); + try { + const workflowResult = await n8nService.triggerMissionCreation(n8nData); + + if (!workflowResult.success) { + return NextResponse.json({ + error: 'Failed to create mission resources', + details: workflowResult.error, + code: 'WORKFLOW_ERROR' + }, { status: 500 }); } - // Add volunteers - if (volunteers && Array.isArray(volunteers)) { - volunteers.forEach(userId => { - if (userId) { - missionUsers.push({ - role: 'volontaire', - userId, - missionId: mission.id - }); - } - }); - } - - if (missionUsers.length > 0) { - console.log('Creating mission users:', JSON.stringify(missionUsers, null, 2)); - await prisma.missionUser.createMany({ - data: missionUsers - }); - } - } - - return NextResponse.json(mission); - } catch (error) { - console.error('Error in n8n workflow:', error); - // If there's an error, we should clean up any resources that were created - if (error instanceof Error && error.message.includes('HTTP error! status: 500')) { + return NextResponse.json(workflowResult); + } catch (error) { return NextResponse.json( { error: 'Failed to create mission resources', @@ -348,19 +217,79 @@ export async function POST(request: Request) { { status: 500 } ); } - return NextResponse.json( - { - error: 'Failed to create mission', - details: error instanceof Error ? error.message : String(error), - code: 'MISSION_CREATION_ERROR' - }, - { status: 500 } - ); } + + // Create mission directly (n8n request) + const mission = await prisma.mission.create({ + data: { + name: body.name, + oddScope: body.oddScope || ['default'], + niveau: body.niveau, + intention: body.intention, + missionType: body.missionType, + donneurDOrdre: body.donneurDOrdre, + projection: body.projection, + services: Array.isArray(body.services) ? body.services.filter(Boolean) : [], + profils: Array.isArray(body.profils) ? body.profils.filter(Boolean) : [], + participation: body.participation || 'default', + creatorId: userId, + logo: body.logo || null, + leantimeProjectId: body.leantimeProjectId || null, + outlineCollectionId: body.documentationCollectionId || null, + rocketChatChannelId: body.rocketchatChannelId || null, + giteaRepositoryUrl: body.gitRepoUrl || null, + penpotProjectId: body.penpotProjectId || null + } as Prisma.MissionUncheckedCreateInput + }); + + // Add guardians and volunteers + if (body.guardians || body.volunteers) { + const missionUsers: MissionUserInput[] = []; + + // Add guardians + if (body.guardians) { + Object.entries(body.guardians).forEach(([role, userId]) => { + if (userId) { + missionUsers.push({ + role, + userId: userId as string, + missionId: mission.id + }); + } + }); + } + + // Add volunteers + if (body.volunteers && Array.isArray(body.volunteers)) { + body.volunteers.forEach(userId => { + if (userId) { + missionUsers.push({ + role: 'volontaire', + userId, + missionId: mission.id + }); + } + }); + } + + if (missionUsers.length > 0) { + await prisma.missionUser.createMany({ + data: missionUsers + }); + } + } + + return NextResponse.json({ + message: 'Mission created successfully', + mission + }); } catch (error) { console.error('Error creating mission:', error); return NextResponse.json( - { error: 'Failed to create mission', details: error instanceof Error ? error.message : String(error) }, + { + error: 'Failed to create mission', + details: error instanceof Error ? error.message : String(error) + }, { status: 500 } ); }