Files
glm-hub-solicitudes-de-acce…/glm-hub-solicitudes-de-acceso-y-aprobacion.json

2124 lines
107 KiB
JSON

{
"updatedAt": "2026-08-18T13:05:05.273Z",
"createdAt": "2026-07-29T13:08:05.444Z",
"id": "RaNkDfE2YCrZgCbx",
"name": "GLM Hub - Solicitudes de acceso y aprobación",
"description": null,
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "glm-hub-solicitar-acceso",
"responseMode": "responseNode",
"options": {
"allowedOrigins": "*"
}
},
"id": "6fee8ce5-2837-4683-9b9c-fd1022a87f5c",
"name": "Webhook - Solicitar acceso",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-1920,
896
],
"webhookId": "ad3e98b9-baf2-400f-90e7-dcd4c5c20457"
},
{
"parameters": {
"jsCode": "const source = $json.body ?? $json;\nlet payload = source;\nif (typeof payload === 'string') {\n try { payload = JSON.parse(payload); } catch { payload = {}; }\n}\nconst accessToken = String(payload?.accessToken ?? '').trim();\nconst appId = String(payload?.appId ?? '').trim();\nconst jwtPattern = /^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$/;\nconst uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\nlet error = '';\nif (!jwtPattern.test(accessToken)) error = 'La sesión recibida no es válida.';\nelse if (!uuidPattern.test(appId)) error = 'Selecciona una aplicación válida.';\nreturn [{ json: { valid: !error, error, authorization: `Bearer ${accessToken}`, appId } }];"
},
"id": "05811eaf-72a8-448c-8c01-16b9d9d7a592",
"name": "Validar solicitud de acceso",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1696,
896
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "b5933422-7722-4c98-b34b-262a20d3ac05",
"leftValue": "={{ $json.valid }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "fddbe870-eea1-4b94-9e64-c4aec9c4dad7",
"name": "¿Solicitud de acceso válida?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-1472,
896
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { ok: false, error: $json.error } }}",
"options": {
"responseCode": 400,
"responseHeaders": {
"entries": [
{
"name": "Cache-Control",
"value": "no-store"
}
]
}
}
},
"id": "397ca441-991c-4f5d-9ce8-1df22a9fccc8",
"name": "Responder solicitud inválida",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-1152,
1056
]
},
{
"parameters": {
"method": "POST",
"url": "https://dbit.digitalcompass.agency/rest/v1/rpc/glm_hub_prepare_access_request",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"
},
{
"name": "Authorization",
"value": "={{ $('Validar solicitud de acceso').first().json.authorization }}"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\n p_app_id: $('Validar solicitud de acceso').first().json.appId\n} }}",
"options": {
"response": {
"response": {
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "1315636b-e124-4a59-9308-61f6855c482a",
"name": "Crear solicitud en Supabase",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-688,
816
]
},
{
"parameters": {
"jsCode": "const source = $input.first()?.json ?? {};\n\nconst parseJson = (value) => {\n if (typeof value !== 'string') return value;\n\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n};\n\nlet payload =\n source.body ??\n source.data ??\n source.response?.body ??\n source;\n\npayload = parseJson(payload);\n\nif (Array.isArray(payload)) {\n payload = payload[0] ?? {};\n}\n\nif (\n payload &&\n typeof payload === 'object' &&\n payload.body !== undefined &&\n payload.ok === undefined\n) {\n payload = parseJson(payload.body);\n\n if (Array.isArray(payload)) {\n payload = payload[0] ?? {};\n }\n}\n\nif (payload?.ok !== true) {\n return [\n {\n json: {\n created: false,\n publicError:\n 'La solicitud fue registrada, pero no se pudo enviar el correo. Intenta nuevamente en unos minutos.',\n technicalError: String(\n payload?.error ??\n payload?.message ??\n 'No se pudieron emitir los enlaces de aprobación.'\n )\n }\n }\n ];\n}\n\nconst escapeHtml = (value) =>\n String(value ?? '').replace(\n /[&<>\"']/g,\n (c) =>\n ({\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;'\n })[c]\n );\n\nconst encode = encodeURIComponent;\n\nconst confirmBase =\n 'https://agenteit.digitalcompass.agency/webhook/glm-hub-confirmar-solicitud';\n\nconst requesterName = String(\n payload.requesterName ?? 'Usuario GLM'\n);\n\nconst requesterEmail = String(\n payload.requesterEmail ?? ''\n);\n\nconst appName = String(\n payload.appName ?? 'Aplicación GLM'\n);\n\nconst appCategory = String(\n payload.appCategory ?? ''\n);\n\nconst requestId = String(\n payload.requestId ?? ''\n);\n\nconst approvers = Array.isArray(payload.approvers)\n ? payload.approvers\n : [];\n\nif (approvers.length < 2) {\n return [\n {\n json: {\n created: false,\n publicError:\n 'No se pudo completar el envío de la solicitud. Intenta nuevamente en unos minutos.',\n technicalError: `Se esperaban al menos 2 aprobadores y se recibieron ${approvers.length}.`\n }\n }\n ];\n}\n\nreturn approvers.map((approver) => {\n const approveUrl =\n `${confirmBase}?token=${encode(\n String(approver.token ?? '')\n )}&decision=approved&app=${encode(appName)}`;\n\n const rejectUrl =\n `${confirmBase}?token=${encode(\n String(approver.token ?? '')\n )}&decision=rejected&app=${encode(appName)}`;\n\n const html = `\n<!doctype html>\n<html>\n<body style=\"margin:0;background:#F5F5F5;font-family:Arial,sans-serif;color:#4A4A4A\">\n\n<table\n role=\"presentation\"\n width=\"100%\"\n cellspacing=\"0\"\n cellpadding=\"0\"\n style=\"background:#F5F5F5\"\n>\n <tr>\n <td align=\"center\" style=\"padding:32px 16px\">\n\n <table\n role=\"presentation\"\n width=\"640\"\n cellspacing=\"0\"\n cellpadding=\"0\"\n style=\"\n width:100%;\n max-width:640px;\n background:#FFFFFF;\n border-collapse:collapse;\n border-top:4px solid #6CC24A\n \"\n >\n\n <tr>\n <td style=\"padding:28px 34px 16px\">\n <img\n src=\"https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png\"\n width=\"220\"\n alt=\"GomezLee Marketing\"\n style=\"\n display:block;\n width:220px;\n max-width:100%;\n height:auto\n \"\n >\n </td>\n </tr>\n\n <tr>\n <td style=\"padding:8px 34px 0\">\n\n <div\n style=\"\n font-size:11px;\n font-weight:700;\n letter-spacing:2px;\n color:#4F758B\n \"\n >\n GLM HUB · SOLICITUD DE ACCESO\n </div>\n\n <h1\n style=\"\n margin:12px 0 8px;\n font-size:30px;\n line-height:1.12;\n color:#4F758B\n \"\n >\n Una persona necesita acceso.\n </h1>\n\n <p\n style=\"\n margin:0;\n font-size:15px;\n line-height:1.6;\n color:#4A4A4A\n \"\n >\n Hola ${escapeHtml(approver.name)}, revisa la siguiente solicitud.\n </p>\n\n </td>\n </tr>\n\n <tr>\n <td style=\"padding:24px 34px\">\n\n <table\n role=\"presentation\"\n width=\"100%\"\n cellspacing=\"0\"\n cellpadding=\"0\"\n style=\"\n border-collapse:collapse;\n border:1px solid #D0D0D0\n \"\n >\n\n <tr>\n <td\n style=\"\n width:34%;\n padding:12px 14px;\n background:#EEF6E8;\n border-bottom:1px solid #D0D0D0;\n font-size:13px;\n font-weight:700;\n color:#4F758B\n \"\n >\n Solicitante\n </td>\n\n <td\n style=\"\n padding:12px 14px;\n border-bottom:1px solid #D0D0D0;\n font-size:14px\n \"\n >\n ${escapeHtml(requesterName)}\n </td>\n </tr>\n\n <tr>\n <td\n style=\"\n padding:12px 14px;\n background:#EEF6E8;\n border-bottom:1px solid #D0D0D0;\n font-size:13px;\n font-weight:700;\n color:#4F758B\n \"\n >\n Correo\n </td>\n\n <td\n style=\"\n padding:12px 14px;\n border-bottom:1px solid #D0D0D0;\n font-size:14px\n \"\n >\n ${escapeHtml(requesterEmail)}\n </td>\n </tr>\n\n <tr>\n <td\n style=\"\n padding:12px 14px;\n background:#EEF6E8;\n border-bottom:1px solid #D0D0D0;\n font-size:13px;\n font-weight:700;\n color:#4F758B\n \"\n >\n Aplicación\n </td>\n\n <td\n style=\"\n padding:12px 14px;\n border-bottom:1px solid #D0D0D0;\n font-size:14px;\n font-weight:700;\n color:#4F758B\n \"\n >\n ${escapeHtml(appName)}\n </td>\n </tr>\n\n <tr>\n <td\n style=\"\n padding:12px 14px;\n background:#EEF6E8;\n font-size:13px;\n font-weight:700;\n color:#4F758B\n \"\n >\n Área\n </td>\n\n <td\n style=\"\n padding:12px 14px;\n font-size:14px\n \"\n >\n ${escapeHtml(appCategory)}\n </td>\n </tr>\n\n </table>\n\n </td>\n </tr>\n\n <tr>\n <td style=\"padding:0 34px 30px\">\n\n <p\n style=\"\n margin:0 0 16px;\n font-size:13px;\n line-height:1.55;\n color:#6B8FA3\n \"\n >\n Por seguridad, cada botón abre una pantalla de confirmación.\n La primera decisión confirmada será definitiva para todos los aprobadores.\n </p>\n\n <table\n role=\"presentation\"\n cellspacing=\"0\"\n cellpadding=\"0\"\n >\n <tr>\n\n <td style=\"padding-right:10px\">\n <a\n href=\"${approveUrl}\"\n style=\"\n display:inline-block;\n padding:13px 22px;\n background:#6CC24A;\n color:#FFFFFF;\n text-decoration:none;\n font-size:14px;\n font-weight:700;\n border-radius:3px\n \"\n >\n Aceptar solicitud\n </a>\n </td>\n\n <td>\n <a\n href=\"${rejectUrl}\"\n style=\"\n display:inline-block;\n padding:12px 22px;\n background:#FFFFFF;\n color:#E87722;\n text-decoration:none;\n font-size:14px;\n font-weight:700;\n border:1px solid #E87722;\n border-radius:3px\n \"\n >\n Rechazar solicitud\n </a>\n </td>\n\n </tr>\n </table>\n\n </td>\n </tr>\n\n <tr>\n <td\n style=\"\n padding:18px 34px;\n border-top:1px solid #D0D0D0;\n font-size:11px;\n line-height:1.5;\n color:#6B8FA3\n \"\n >\n GomezLee Marketing · Uso interno\n </td>\n </tr>\n\n </table>\n\n </td>\n </tr>\n</table>\n\n</body>\n</html>`;\n\n return {\n json: {\n created: true,\n requestId,\n sendTo: String(approver.email ?? ''),\n subject: `Solicitud de acceso | ${appName} | ${requesterName}`,\n html\n }\n };\n});"
},
"id": "b38bcf45-4f58-4997-87e2-247c6478bdc2",
"name": "Preparar correos de aprobación",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
448,
768
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "02a636e0-63a0-4a56-b108-65029e555646",
"leftValue": "={{ $json.created }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "d1c724f7-4048-4545-b055-55d355fbe4bc",
"name": "¿Solicitud registrada?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
688,
768
]
},
{
"parameters": {
"sendTo": "={{ $json.sendTo }}",
"subject": "={{ $json.subject }}",
"message": "={{ $json.html }}",
"options": {
"appendAttribution": false,
"senderName": "GLM Hub"
}
},
"id": "b1a5605e-0ad9-4cae-8290-dec2427f645f",
"name": "Enviar solicitud a aprobadores",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
928,
672
],
"webhookId": "7dc65e7b-4fb6-4a2f-b685-34171dd9ce1a",
"credentials": {
"gmailOAuth2": {
"id": "UDcO1FLJqA453V2D",
"name": "Gmail account 3"
}
}
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { ok: true, message: 'La solicitud fue enviada correctamente a las personas responsables de aprobación.', requestId: $('Preparar correos de aprobación').first().json.requestId } }}",
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Cache-Control",
"value": "no-store"
}
]
}
}
},
"id": "8280f9b8-18ee-456c-8e71-2c923f8e60ec",
"name": "Responder solicitud enviada",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
1168,
672
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ {\n ok: false,\n error: $json.publicError || 'No pudimos enviar la solicitud. Intenta nuevamente.'\n} }}",
"options": {
"responseCode": 409,
"responseHeaders": {
"entries": [
{
"name": "Cache-Control",
"value": "no-store"
}
]
}
}
},
"id": "59696c39-a80c-4548-80a8-162d63ea1346",
"name": "Responder error al registrar",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
928,
880
]
},
{
"parameters": {
"path": "glm-hub-confirmar-solicitud",
"responseMode": "responseNode",
"options": {}
},
"id": "e0152ea1-39a8-49e1-add4-ec5f83b7b288",
"name": "Webhook - Confirmar decisión",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-560,
1600
],
"webhookId": "cba1c38c-37bc-4acb-bd61-48035604a5c4"
},
{
"parameters": {
"jsCode": "const query = $json.query ?? {};\nconst token = String(query.token ?? '').trim();\nconst decision = String(query.decision ?? '').trim().toLowerCase();\nconst appName = String(query.app ?? 'aplicación GLM').trim();\nconst valid = /^[0-9a-f]{64}$/i.test(token) && ['approved','rejected'].includes(decision);\nconst escapeHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\nconst actionUrl = 'https://agenteit.digitalcompass.agency/webhook/glm-hub-resolver-solicitud';\nconst approved = decision === 'approved';\nconst title = approved ? 'Confirmar aprobación' : 'Confirmar rechazo';\nconst actionLabel = approved ? 'Sí, aprobar solicitud' : 'Sí, rechazar solicitud';\nconst actionColor = approved ? '#6CC24A' : '#E87722';\nconst html = valid\n ? `<!doctype html><html lang=\"es\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>${title} · GLM Hub</title></head><body style=\"margin:0;background:#F5F5F5;font-family:Arial,sans-serif;color:#4A4A4A\"><main style=\"min-height:100vh;display:grid;place-items:center;padding:24px\"><section style=\"width:100%;max-width:560px;background:#FFFFFF;border-top:4px solid #6CC24A;padding:36px;box-sizing:border-box;box-shadow:0 24px 70px rgba(0,0,0,.14)\"><img src=\"https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM.png\" width=\"220\" alt=\"GomezLee Marketing\" style=\"display:block;width:220px;max-width:100%;height:auto\"><p style=\"margin:28px 0 8px;font-size:11px;font-weight:700;letter-spacing:2px;color:#4F758B\">GLM HUB · DECISIÓN DE ACCESO</p><h1 style=\"margin:0;color:#4F758B;font-size:34px;line-height:1.1\">${title}</h1><p style=\"margin:16px 0 24px;font-size:15px;line-height:1.6\">Vas a ${approved ? 'aprobar' : 'rechazar'} la solicitud de acceso a <strong>${escapeHtml(appName)}</strong>. La decisión será definitiva y los otros enlaces quedarán sin efecto.</p><form method=\"post\" action=\"${actionUrl}\"><input type=\"hidden\" name=\"token\" value=\"${escapeHtml(token)}\"><input type=\"hidden\" name=\"decision\" value=\"${escapeHtml(decision)}\"><button type=\"submit\" style=\"width:100%;min-height:50px;border:0;border-radius:3px;background:${actionColor};color:#FFFFFF;font-family:Arial,sans-serif;font-size:15px;font-weight:700;cursor:pointer\">${actionLabel}</button></form><p style=\"margin:18px 0 0;font-size:12px;line-height:1.5;color:#6B8FA3\">Cierra esta pestaña para cancelar sin registrar ninguna decisión.</p></section></main></body></html>`\n : `<!doctype html><html lang=\"es\"><body style=\"margin:0;background:#F5F5F5;font-family:Arial,sans-serif\"><main style=\"min-height:100vh;display:grid;place-items:center;padding:24px\"><section style=\"max-width:520px;background:#FFFFFF;border-top:4px solid #E87722;padding:36px\"><img src=\"https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM.png\" width=\"220\" alt=\"GomezLee Marketing\"><h1 style=\"color:#4F758B\">Enlace no válido</h1><p style=\"color:#4A4A4A;line-height:1.6\">El enlace está incompleto o no corresponde a una solicitud de GLM Hub.</p></section></main></body></html>`;\nreturn [{ json: { html } }];"
},
"id": "1cafd88f-b081-45fe-ab11-9de66d2337e0",
"name": "Construir página de confirmación",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-336,
1600
]
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $json.html }}",
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/html; charset=utf-8"
},
{
"name": "Cache-Control",
"value": "no-store"
},
{
"name": "X-Frame-Options",
"value": "DENY"
}
]
}
}
},
"id": "7827c03c-dae8-4b9b-9c31-7f2a3a1e7dfe",
"name": "Mostrar confirmación",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-112,
1600
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "glm-hub-resolver-solicitud",
"responseMode": "responseNode",
"options": {}
},
"id": "1cc0149c-206d-479e-b432-d9060bebb79d",
"name": "Webhook - Resolver solicitud",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-1536,
2288
],
"webhookId": "54eeee1b-b24b-46ae-84dc-70c4103e3a3a"
},
{
"parameters": {
"jsCode": "const source = $json.body ?? $json;\nlet payload = source;\nif (typeof payload === 'string') {\n try {\n payload = Object.fromEntries(new URLSearchParams(payload));\n } catch { payload = {}; }\n}\nconst token = String(payload?.token ?? '').trim();\nconst decision = String(payload?.decision ?? '').trim().toLowerCase();\nlet error = '';\nif (!/^[0-9a-f]{64}$/i.test(token)) error = 'El enlace de decisión no es válido.';\nelse if (!['approved','rejected'].includes(decision)) error = 'La decisión recibida no es válida.';\nreturn [{ json: { valid: !error, error, token, decision } }];"
},
"id": "864d54fe-72f0-488a-bff6-0de0d9044308",
"name": "Validar decisión",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1312,
2288
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "5c42c35a-903b-4a18-a7ef-aab6e6bb5c26",
"leftValue": "={{ $json.valid }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "07f79dcf-3d55-4122-9713-f3a1009e4657",
"name": "¿Decisión válida?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-1088,
2288
]
},
{
"parameters": {
"jsCode": "const message = String($json.error ?? 'El enlace de decisión no es válido.');\nconst escapeHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\nconst html = `<!doctype html><html lang=\"es\"><body style=\"margin:0;background:#F5F5F5;font-family:Arial,sans-serif\"><main style=\"min-height:100vh;display:grid;place-items:center;padding:24px\"><section style=\"max-width:520px;background:#FFFFFF;border-top:4px solid #E87722;padding:36px\"><img src=\"https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM.png\" width=\"220\" alt=\"GomezLee Marketing\"><h1 style=\"color:#4F758B\">No se pudo procesar</h1><p style=\"color:#4A4A4A;line-height:1.6\">${escapeHtml(message)}</p></section></main></body></html>`;\nreturn [{ json: { html } }];"
},
"id": "da7d60fb-beb0-4a3e-94e7-31cd643b40c7",
"name": "Preparar decisión inválida",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-864,
2480
]
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $json.html }}",
"options": {
"responseCode": 400,
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/html; charset=utf-8"
},
{
"name": "Cache-Control",
"value": "no-store"
},
{
"name": "X-Frame-Options",
"value": "DENY"
}
]
}
}
},
"id": "1154375e-9680-4582-8bdd-514b15d27726",
"name": "Responder decisión inválida",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-624,
2480
]
},
{
"parameters": {
"method": "POST",
"url": "https://dbit.digitalcompass.agency/rest/v1/rpc/glm_hub_decide_access_request",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Authorization",
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Accept",
"value": "application/json"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\n p_token: $('Validar decisión').first().json.token,\n p_decision: $('Validar decisión').first().json.decision\n} }}",
"options": {
"response": {
"response": {
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "f453b62b-82ef-49e1-8b7a-e50a51a14fa3",
"name": "Registrar decisión en Supabase",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-864,
2208
]
},
{
"parameters": {
"jsCode": "const source = $input.first()?.json ?? {};\nlet payload = source.body ?? source;\nif (typeof payload === 'string') { try { payload = JSON.parse(payload); } catch { payload = {}; } }\nif (Array.isArray(payload)) payload = payload[0] ?? {};\nconst escapeHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\nconst status = String(payload.status ?? '');\nconst approved = status === 'approved';\nconst statusLabel = approved ? 'APROBADA' : status === 'rejected' ? 'RECHAZADA' : 'SIN PROCESAR';\nconst statusColor = approved ? '#6CC24A' : '#E87722';\nconst ok = payload.ok === true;\nconst alreadyDecided = payload.alreadyDecided === true;\nconst requesterName = String(payload.requesterName ?? '');\nconst requesterEmail = String(payload.requesterEmail ?? '');\nconst appName = String(payload.appName ?? 'Aplicación GLM');\nconst decidedByName = String(payload.decidedByName ?? '');\nconst decidedByEmail = String(payload.decidedByEmail ?? '');\nconst responseTitle = ok ? (approved ? 'Solicitud aprobada' : 'Solicitud rechazada') : alreadyDecided ? 'Solicitud ya atendida' : 'No se pudo procesar';\nconst responseMessage = ok\n ? `La decisión fue registrada. IT Support recibirá los datos para continuar con el acceso.`\n : alreadyDecided\n ? `Esta solicitud ya fue ${status === 'approved' ? 'aprobada' : 'rechazada'} por ${escapeHtml(decidedByName || decidedByEmail || 'otra persona')}. No se realizó ningún cambio adicional.`\n : escapeHtml(payload.error ?? 'El enlace no pudo ser validado.');\nconst responseHtml = `<!doctype html><html lang=\"es\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>${responseTitle} · GLM Hub</title></head><body style=\"margin:0;background:#F5F5F5;font-family:Arial,sans-serif;color:#4A4A4A\"><main style=\"min-height:100vh;display:grid;place-items:center;padding:24px\"><section style=\"width:100%;max-width:560px;background:#FFFFFF;border-top:4px solid ${ok ? statusColor : '#4F758B'};padding:36px;box-sizing:border-box;box-shadow:0 24px 70px rgba(0,0,0,.14)\"><img src=\"https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM.png\" width=\"220\" alt=\"GomezLee Marketing\" style=\"display:block;width:220px;max-width:100%;height:auto\"><p style=\"margin:28px 0 8px;font-size:11px;font-weight:700;letter-spacing:2px;color:#4F758B\">GLM HUB · SOLICITUD DE ACCESO</p><h1 style=\"margin:0;color:#4F758B;font-size:34px;line-height:1.1\">${responseTitle}</h1><p style=\"margin:16px 0 0;font-size:15px;line-height:1.65\">${responseMessage}</p><p style=\"margin:24px 0 0;font-size:12px;color:#6B8FA3\">Ya puedes cerrar esta pestaña.</p></section></main></body></html>`;\nconst itHtml = ok ? `<!doctype html><html><body style=\"margin:0;background:#F5F5F5;font-family:Arial,sans-serif;color:#4A4A4A\"><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"background:#F5F5F5\"><tr><td align=\"center\" style=\"padding:32px 16px\"><table role=\"presentation\" width=\"680\" cellspacing=\"0\" cellpadding=\"0\" style=\"width:100%;max-width:680px;background:#FFFFFF;border-collapse:collapse;border-top:4px solid ${statusColor}\"><tr><td style=\"padding:28px 34px 16px\"><img src=\"https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM.png\" width=\"220\" alt=\"GomezLee Marketing\" style=\"display:block;width:220px;max-width:100%;height:auto\"></td></tr><tr><td style=\"padding:8px 34px 0\"><div style=\"font-size:11px;font-weight:700;letter-spacing:2px;color:#4F758B\">GLM HUB · DECISIÓN REGISTRADA</div><h1 style=\"margin:12px 0 8px;font-size:30px;line-height:1.12;color:#4F758B\">Solicitud ${statusLabel.toLowerCase()}.</h1><p style=\"margin:0;font-size:15px;line-height:1.6\">IT Support debe continuar según la decisión indicada.</p></td></tr><tr><td style=\"padding:24px 34px\"><div style=\"display:inline-block;margin-bottom:16px;padding:7px 12px;border:1px solid ${statusColor};color:${statusColor};font-size:12px;font-weight:700;letter-spacing:1px\">${statusLabel}</div><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:collapse;border:1px solid #D0D0D0\"><tr><td style=\"width:38%;padding:12px 14px;background:#EEF6E8;border-bottom:1px solid #D0D0D0;font-size:13px;font-weight:700;color:#4F758B\">Persona solicitante</td><td style=\"padding:12px 14px;border-bottom:1px solid #D0D0D0;font-size:14px\">${escapeHtml(requesterName)}</td></tr><tr><td style=\"padding:12px 14px;background:#EEF6E8;border-bottom:1px solid #D0D0D0;font-size:13px;font-weight:700;color:#4F758B\">Correo solicitante</td><td style=\"padding:12px 14px;border-bottom:1px solid #D0D0D0;font-size:14px\">${escapeHtml(requesterEmail)}</td></tr><tr><td style=\"padding:12px 14px;background:#EEF6E8;border-bottom:1px solid #D0D0D0;font-size:13px;font-weight:700;color:#4F758B\">Aplicación</td><td style=\"padding:12px 14px;border-bottom:1px solid #D0D0D0;font-size:14px;font-weight:700;color:#4F758B\">${escapeHtml(appName)}</td></tr><tr><td style=\"padding:12px 14px;background:#EEF6E8;border-bottom:1px solid #D0D0D0;font-size:13px;font-weight:700;color:#4F758B\">Decisión</td><td style=\"padding:12px 14px;border-bottom:1px solid #D0D0D0;font-size:14px;font-weight:700;color:${statusColor}\">${statusLabel}</td></tr><tr><td style=\"padding:12px 14px;background:#EEF6E8;border-bottom:1px solid #D0D0D0;font-size:13px;font-weight:700;color:#4F758B\">Decidido por</td><td style=\"padding:12px 14px;border-bottom:1px solid #D0D0D0;font-size:14px\">${escapeHtml(decidedByName)}</td></tr><tr><td style=\"padding:12px 14px;background:#EEF6E8;font-size:13px;font-weight:700;color:#4F758B\">Correo de quien decidió</td><td style=\"padding:12px 14px;font-size:14px\">${escapeHtml(decidedByEmail)}</td></tr></table></td></tr><tr><td style=\"padding:18px 34px;border-top:1px solid #D0D0D0;font-size:11px;line-height:1.5;color:#6B8FA3\">GomezLee Marketing · GLM Hub · Uso interno</td></tr></table></td></tr></table></body></html>` : '';\nreturn [{ json: { notifyIt: ok, responseHtml, itHtml, subject: `Acceso ${approved ? 'aprobado' : 'rechazado'} | ${appName} | ${requesterName}` } }];"
},
"id": "6913ff01-1482-4413-964f-eff10fa4a64e",
"name": "Preparar resultado de decisión",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
352,
2224
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "2ef854a5-d270-49bf-9a6a-5bb92aeab434",
"leftValue": "={{ $json.notifyIt }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "343ab147-11df-4030-96b9-b750e6151100",
"name": "¿Notificar a IT Support?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
592,
2224
]
},
{
"parameters": {
"sendTo": "iaracena@gomezleemarketing.com, ethen@gomezleemarketing.com, lmatos@gomezleemarketing.com",
"subject": "={{ $json.subject }}",
"message": "={{ $json.itHtml }}",
"options": {
"appendAttribution": false,
"senderName": "GLM Hub"
}
},
"id": "c1be686a-a5a6-4285-abbd-bf9ed90247d0",
"name": "Enviar decisión a IT Support",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
832,
2128
],
"webhookId": "234c8e3b-b904-41b8-8a74-9ca835f759df",
"credentials": {
"gmailOAuth2": {
"id": "UDcO1FLJqA453V2D",
"name": "Gmail account 3"
}
}
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $('Preparar resultado de decisión').first().json.responseHtml }}",
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/html; charset=utf-8"
},
{
"name": "Cache-Control",
"value": "no-store"
},
{
"name": "X-Frame-Options",
"value": "DENY"
}
]
}
}
},
"id": "f3f941b3-b902-4ed6-abd4-ec3e00baee36",
"name": "Mostrar resultado final",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
1072,
2224
]
},
{
"parameters": {
"jsCode": "const source = $input.first()?.json ?? {};\n\nconst parseJson = (value) => {\n if (typeof value !== 'string') {\n return value;\n }\n\n try {\n return JSON.parse(value);\n } catch {\n return {};\n }\n};\n\nlet payload = source;\n\n// Compatibilidad por si n8n todavía entrega algún envoltorio.\nif (source.body !== undefined) {\n payload = source.body;\n} else if (source.data !== undefined) {\n payload = source.data;\n}\n\npayload = parseJson(payload);\n\nif (Array.isArray(payload)) {\n payload = payload[0] ?? {};\n}\n\nconst registered = payload?.ok === true;\nconst code = String(payload?.code ?? '').toUpperCase();\nconst internalMessage = String(\n payload?.error ??\n payload?.message ??\n ''\n).trim();\n\nconst normalizedMessage = internalMessage.toLowerCase();\n\nlet publicError = 'No pudimos enviar la solicitud. Intenta nuevamente.';\n\nif (\n code === 'PENDING_EXISTS' ||\n normalizedMessage.includes('solicitud pendiente')\n) {\n publicError = 'Ya tienes una solicitud pendiente para esta aplicación.';\n} else if (\n normalizedMessage.includes('sesión') ||\n normalizedMessage.includes('jwt')\n) {\n publicError = 'Tu sesión venció. Cierra sesión e inicia nuevamente.';\n} else if (\n normalizedMessage.includes('administradores no necesitan')\n) {\n publicError =\n 'Tu cuenta administrativa no necesita solicitar acceso desde el Hub.';\n} else if (\n normalizedMessage.includes('no está autorizado')\n) {\n publicError =\n 'Tu cuenta no está habilitada para enviar solicitudes de acceso.';\n} else if (\n normalizedMessage.includes('ya no está publicada')\n) {\n publicError =\n 'La aplicación seleccionada ya no está disponible.';\n}\n\nreturn [\n {\n json: {\n registered,\n publicError,\n internalMessage,\n requestId: String(payload?.requestId ?? ''),\n requesterName: String(payload?.requesterName ?? ''),\n requesterEmail: String(payload?.requesterEmail ?? ''),\n appName: String(payload?.appName ?? ''),\n appCategory: String(payload?.appCategory ?? '')\n }\n }\n];"
},
"id": "5e121030-2188-415d-8013-a54d58fedc91",
"name": "Confirmar registro de solicitud",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-480,
816
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "aa091e1c-09f8-4cdb-b8ac-3cbc4a7a33c8",
"leftValue": "={{ $json.registered }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "f722416a-53e0-403d-a301-8bfe4d43de60",
"name": "¿Solicitud registrada en Supabase?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-256,
816
]
},
{
"parameters": {
"method": "POST",
"url": "https://dbit.digitalcompass.agency/rest/v1/rpc/glm_hub_issue_access_request_tokens",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Authorization",
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Accept",
"value": "application/json"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\n p_request_id: $('Confirmar registro de solicitud').first().json.requestId\n} }}",
"options": {
"response": {
"response": {
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "dfe96316-ab43-49e2-b9fb-384d34c5389c",
"name": "Emitir enlaces de aprobación",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
208,
768
]
},
{
"parameters": {
"content": "## 1. RECEPCIÓN Y VALIDACIÓN\n\nEste bloque recibe desde GLM Hub una nueva solicitud de acceso y valida que la información obligatoria sea correcta.\n\n- Verifica los datos del usuario y de la aplicación solicitada.\n- Rechaza inmediatamente solicitudes incompletas o inválidas.\n- Solo las solicitudes válidas continúan hacia el registro en Supabase.",
"height": 560,
"width": 1104,
"color": 6
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-2016,
688
],
"id": "97451dbc-cd46-40e8-911b-7c4cf3e648f4",
"name": "Sticky Note"
},
{
"parameters": {
"content": "## 2. REGISTRO DE LA SOLICITUD\n\nGuarda la solicitud de acceso en Supabase y confirma que el registro se haya creado correctamente.\n\n- Almacena la información de la solicitud y su estado inicial.\n- Recupera y normaliza los datos devueltos por Supabase.\n- Solo permite continuar cuando existe una confirmación válida del registro.",
"height": 384,
"width": 736,
"color": 5
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-800,
640
],
"id": "5f36aafe-12ce-431d-90e8-2b49cf214f1d",
"name": "Sticky Note1"
},
{
"parameters": {
"content": "## 3. ENVÍO A LOS APROBADORES\n\nGenera los enlaces de decisión, prepara los correos y notifica a las personas responsables de aprobar o rechazar la solicitud.\n\n- Mantiene como aprobadores base a Isaac Aracena, José Leopoldo Gómez y Máximo Gómez.\n- Agrega automáticamente al jefe inmediato del solicitante cuando existe `supervisor_email` en `empleados_glm`.\n- Evita duplicados cuando el jefe ya es uno de los aprobadores base.\n- Construye enlaces únicos para la aprobación y el rechazo.\n- Envía un correo personalizado a cada aprobador mediante Gmail.\n- La primera decisión registrada continúa siendo definitiva para todos los enlaces.",
"height": 560,
"width": 1264,
"color": "#357C27"
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
112,
480
],
"id": "bab6dbac-ae4b-46ee-b31e-e894aa1fc20a",
"name": "Sticky Note2"
},
{
"parameters": {
"content": "## 4. CONFIRMACIÓN DE LA DECISIÓN\n\nRecibe al aprobador desde el enlace enviado por correo y muestra una página previa antes de registrar la decisión definitiva.\n\n- Lee los parámetros incluidos en el enlace de aprobación o rechazo.\n- Construye una página con los datos de la solicitud.\n- Solicita una confirmación final para evitar decisiones accidentales.",
"height": 352,
"width": 1062,
"color": "#8F622D"
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-800,
1424
],
"id": "9115ee9e-a2ce-419f-a783-62cfe268c8fd",
"name": "Sticky Note3"
},
{
"parameters": {
"content": "## 5. VALIDACIÓN Y REGISTRO DE LA DECISIÓN\n\nValida la decisión confirmada por el aprobador y actualiza la solicitud correspondiente en Supabase.\n\n- Comprueba que la solicitud, el enlace y la decisión sean válidos.\n- Registra en Supabase la aprobación o el rechazo.\n- Impide procesar decisiones incorrectas, incompletas o manipuladas.\n- Muestra una respuesta controlada cuando la decisión no puede ser aceptada.",
"height": 624,
"width": 1184,
"color": "#52500F"
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-1616,
2032
],
"id": "68fac86e-bbee-4648-b2ed-bfe3b9e9829c",
"name": "Sticky Note4"
},
{
"parameters": {
"content": "## 6. NOTIFICACIÓN Y RESULTADO FINAL\n\nPrepara el resultado de la decisión, determina si se necesita intervención de IT Support y finaliza el proceso.\n\n- Construye el mensaje final de aprobación o rechazo.\n- Evalúa si la decisión debe notificarse al equipo de IT.\n- Envía por Gmail la información necesaria cuando corresponde.\n- Muestra al aprobador el resultado definitivo de la operación.",
"height": 432,
"width": 1104,
"color": "#276D4F"
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
192,
1968
],
"id": "024b61e1-bbc8-4838-9afe-7ee36445d0db",
"name": "Sticky Note5"
}
],
"connections": {
"Webhook - Solicitar acceso": {
"main": [
[
{
"node": "Validar solicitud de acceso",
"type": "main",
"index": 0
}
]
]
},
"Validar solicitud de acceso": {
"main": [
[
{
"node": "¿Solicitud de acceso válida?",
"type": "main",
"index": 0
}
]
]
},
"¿Solicitud de acceso válida?": {
"main": [
[
{
"node": "Crear solicitud en Supabase",
"type": "main",
"index": 0
}
],
[
{
"node": "Responder solicitud inválida",
"type": "main",
"index": 0
}
]
]
},
"Crear solicitud en Supabase": {
"main": [
[
{
"node": "Confirmar registro de solicitud",
"type": "main",
"index": 0
}
]
]
},
"Preparar correos de aprobación": {
"main": [
[
{
"node": "¿Solicitud registrada?",
"type": "main",
"index": 0
}
]
]
},
"¿Solicitud registrada?": {
"main": [
[
{
"node": "Enviar solicitud a aprobadores",
"type": "main",
"index": 0
}
],
[
{
"node": "Responder error al registrar",
"type": "main",
"index": 0
}
]
]
},
"Enviar solicitud a aprobadores": {
"main": [
[
{
"node": "Responder solicitud enviada",
"type": "main",
"index": 0
}
]
]
},
"Webhook - Confirmar decisión": {
"main": [
[
{
"node": "Construir página de confirmación",
"type": "main",
"index": 0
}
]
]
},
"Construir página de confirmación": {
"main": [
[
{
"node": "Mostrar confirmación",
"type": "main",
"index": 0
}
]
]
},
"Webhook - Resolver solicitud": {
"main": [
[
{
"node": "Validar decisión",
"type": "main",
"index": 0
}
]
]
},
"Validar decisión": {
"main": [
[
{
"node": "¿Decisión válida?",
"type": "main",
"index": 0
}
]
]
},
"¿Decisión válida?": {
"main": [
[
{
"node": "Registrar decisión en Supabase",
"type": "main",
"index": 0
}
],
[
{
"node": "Preparar decisión inválida",
"type": "main",
"index": 0
}
]
]
},
"Preparar decisión inválida": {
"main": [
[
{
"node": "Responder decisión inválida",
"type": "main",
"index": 0
}
]
]
},
"Registrar decisión en Supabase": {
"main": [
[
{
"node": "Preparar resultado de decisión",
"type": "main",
"index": 0
}
]
]
},
"Preparar resultado de decisión": {
"main": [
[
{
"node": "¿Notificar a IT Support?",
"type": "main",
"index": 0
}
]
]
},
"¿Notificar a IT Support?": {
"main": [
[
{
"node": "Enviar decisión a IT Support",
"type": "main",
"index": 0
}
],
[
{
"node": "Mostrar resultado final",
"type": "main",
"index": 0
}
]
]
},
"Enviar decisión a IT Support": {
"main": [
[
{
"node": "Mostrar resultado final",
"type": "main",
"index": 0
}
]
]
},
"Confirmar registro de solicitud": {
"main": [
[
{
"node": "¿Solicitud registrada en Supabase?",
"type": "main",
"index": 0
}
]
]
},
"¿Solicitud registrada en Supabase?": {
"main": [
[
{
"node": "Emitir enlaces de aprobación",
"type": "main",
"index": 0
}
],
[
{
"node": "Responder error al registrar",
"type": "main",
"index": 0
}
]
]
},
"Emitir enlaces de aprobación": {
"main": [
[
{
"node": "Preparar correos de aprobación",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": true
},
"staticData": null,
"meta": {
"templateCredsSetupCompleted": true
},
"versionId": "9b79c73b-73e6-43ea-bf12-90c30f7fb7c1",
"activeVersionId": "9b79c73b-73e6-43ea-bf12-90c30f7fb7c1",
"versionCounter": 152,
"triggerCount": 3,
"shared": [
{
"updatedAt": "2026-07-29T13:08:05.454Z",
"createdAt": "2026-07-29T13:08:05.454Z",
"role": "workflow:owner",
"workflowId": "RaNkDfE2YCrZgCbx",
"projectId": "PJpTANzTXIFibWsW",
"project": {
"updatedAt": "2026-04-22T14:25:09.686Z",
"createdAt": "2026-04-22T14:22:54.790Z",
"id": "PJpTANzTXIFibWsW",
"name": "Isaac Aracena <iaracena@gomezleemarketing.com>",
"type": "personal",
"icon": null,
"description": null,
"creatorId": "0a88c0b1-928e-4412-896e-c5d1c99b2029"
}
}
],
"tags": [],
"activeVersion": {
"updatedAt": "2026-08-18T13:05:10.000Z",
"createdAt": "2026-08-18T13:05:05.276Z",
"versionId": "9b79c73b-73e6-43ea-bf12-90c30f7fb7c1",
"workflowId": "RaNkDfE2YCrZgCbx",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "glm-hub-solicitar-acceso",
"responseMode": "responseNode",
"options": {
"allowedOrigins": "*"
}
},
"id": "6fee8ce5-2837-4683-9b9c-fd1022a87f5c",
"name": "Webhook - Solicitar acceso",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-1920,
896
],
"webhookId": "ad3e98b9-baf2-400f-90e7-dcd4c5c20457"
},
{
"parameters": {
"jsCode": "const source = $json.body ?? $json;\nlet payload = source;\nif (typeof payload === 'string') {\n try { payload = JSON.parse(payload); } catch { payload = {}; }\n}\nconst accessToken = String(payload?.accessToken ?? '').trim();\nconst appId = String(payload?.appId ?? '').trim();\nconst jwtPattern = /^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$/;\nconst uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\nlet error = '';\nif (!jwtPattern.test(accessToken)) error = 'La sesión recibida no es válida.';\nelse if (!uuidPattern.test(appId)) error = 'Selecciona una aplicación válida.';\nreturn [{ json: { valid: !error, error, authorization: `Bearer ${accessToken}`, appId } }];"
},
"id": "05811eaf-72a8-448c-8c01-16b9d9d7a592",
"name": "Validar solicitud de acceso",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1696,
896
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "b5933422-7722-4c98-b34b-262a20d3ac05",
"leftValue": "={{ $json.valid }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "fddbe870-eea1-4b94-9e64-c4aec9c4dad7",
"name": "¿Solicitud de acceso válida?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-1472,
896
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { ok: false, error: $json.error } }}",
"options": {
"responseCode": 400,
"responseHeaders": {
"entries": [
{
"name": "Cache-Control",
"value": "no-store"
}
]
}
}
},
"id": "397ca441-991c-4f5d-9ce8-1df22a9fccc8",
"name": "Responder solicitud inválida",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-1152,
1056
]
},
{
"parameters": {
"method": "POST",
"url": "https://dbit.digitalcompass.agency/rest/v1/rpc/glm_hub_prepare_access_request",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"
},
{
"name": "Authorization",
"value": "={{ $('Validar solicitud de acceso').first().json.authorization }}"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\n p_app_id: $('Validar solicitud de acceso').first().json.appId\n} }}",
"options": {
"response": {
"response": {
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "1315636b-e124-4a59-9308-61f6855c482a",
"name": "Crear solicitud en Supabase",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-688,
816
]
},
{
"parameters": {
"jsCode": "const source = $input.first()?.json ?? {};\n\nconst parseJson = (value) => {\n if (typeof value !== 'string') return value;\n\n try {\n return JSON.parse(value);\n } catch {\n return value;\n }\n};\n\nlet payload =\n source.body ??\n source.data ??\n source.response?.body ??\n source;\n\npayload = parseJson(payload);\n\nif (Array.isArray(payload)) {\n payload = payload[0] ?? {};\n}\n\nif (\n payload &&\n typeof payload === 'object' &&\n payload.body !== undefined &&\n payload.ok === undefined\n) {\n payload = parseJson(payload.body);\n\n if (Array.isArray(payload)) {\n payload = payload[0] ?? {};\n }\n}\n\nif (payload?.ok !== true) {\n return [\n {\n json: {\n created: false,\n publicError:\n 'La solicitud fue registrada, pero no se pudo enviar el correo. Intenta nuevamente en unos minutos.',\n technicalError: String(\n payload?.error ??\n payload?.message ??\n 'No se pudieron emitir los enlaces de aprobación.'\n )\n }\n }\n ];\n}\n\nconst escapeHtml = (value) =>\n String(value ?? '').replace(\n /[&<>\"']/g,\n (c) =>\n ({\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;'\n })[c]\n );\n\nconst encode = encodeURIComponent;\n\nconst confirmBase =\n 'https://agenteit.digitalcompass.agency/webhook/glm-hub-confirmar-solicitud';\n\nconst requesterName = String(\n payload.requesterName ?? 'Usuario GLM'\n);\n\nconst requesterEmail = String(\n payload.requesterEmail ?? ''\n);\n\nconst appName = String(\n payload.appName ?? 'Aplicación GLM'\n);\n\nconst appCategory = String(\n payload.appCategory ?? ''\n);\n\nconst requestId = String(\n payload.requestId ?? ''\n);\n\nconst approvers = Array.isArray(payload.approvers)\n ? payload.approvers\n : [];\n\nif (approvers.length < 2) {\n return [\n {\n json: {\n created: false,\n publicError:\n 'No se pudo completar el envío de la solicitud. Intenta nuevamente en unos minutos.',\n technicalError: `Se esperaban al menos 2 aprobadores y se recibieron ${approvers.length}.`\n }\n }\n ];\n}\n\nreturn approvers.map((approver) => {\n const approveUrl =\n `${confirmBase}?token=${encode(\n String(approver.token ?? '')\n )}&decision=approved&app=${encode(appName)}`;\n\n const rejectUrl =\n `${confirmBase}?token=${encode(\n String(approver.token ?? '')\n )}&decision=rejected&app=${encode(appName)}`;\n\n const html = `\n<!doctype html>\n<html>\n<body style=\"margin:0;background:#F5F5F5;font-family:Arial,sans-serif;color:#4A4A4A\">\n\n<table\n role=\"presentation\"\n width=\"100%\"\n cellspacing=\"0\"\n cellpadding=\"0\"\n style=\"background:#F5F5F5\"\n>\n <tr>\n <td align=\"center\" style=\"padding:32px 16px\">\n\n <table\n role=\"presentation\"\n width=\"640\"\n cellspacing=\"0\"\n cellpadding=\"0\"\n style=\"\n width:100%;\n max-width:640px;\n background:#FFFFFF;\n border-collapse:collapse;\n border-top:4px solid #6CC24A\n \"\n >\n\n <tr>\n <td style=\"padding:28px 34px 16px\">\n <img\n src=\"https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_completo.png\"\n width=\"220\"\n alt=\"GomezLee Marketing\"\n style=\"\n display:block;\n width:220px;\n max-width:100%;\n height:auto\n \"\n >\n </td>\n </tr>\n\n <tr>\n <td style=\"padding:8px 34px 0\">\n\n <div\n style=\"\n font-size:11px;\n font-weight:700;\n letter-spacing:2px;\n color:#4F758B\n \"\n >\n GLM HUB · SOLICITUD DE ACCESO\n </div>\n\n <h1\n style=\"\n margin:12px 0 8px;\n font-size:30px;\n line-height:1.12;\n color:#4F758B\n \"\n >\n Una persona necesita acceso.\n </h1>\n\n <p\n style=\"\n margin:0;\n font-size:15px;\n line-height:1.6;\n color:#4A4A4A\n \"\n >\n Hola ${escapeHtml(approver.name)}, revisa la siguiente solicitud.\n </p>\n\n </td>\n </tr>\n\n <tr>\n <td style=\"padding:24px 34px\">\n\n <table\n role=\"presentation\"\n width=\"100%\"\n cellspacing=\"0\"\n cellpadding=\"0\"\n style=\"\n border-collapse:collapse;\n border:1px solid #D0D0D0\n \"\n >\n\n <tr>\n <td\n style=\"\n width:34%;\n padding:12px 14px;\n background:#EEF6E8;\n border-bottom:1px solid #D0D0D0;\n font-size:13px;\n font-weight:700;\n color:#4F758B\n \"\n >\n Solicitante\n </td>\n\n <td\n style=\"\n padding:12px 14px;\n border-bottom:1px solid #D0D0D0;\n font-size:14px\n \"\n >\n ${escapeHtml(requesterName)}\n </td>\n </tr>\n\n <tr>\n <td\n style=\"\n padding:12px 14px;\n background:#EEF6E8;\n border-bottom:1px solid #D0D0D0;\n font-size:13px;\n font-weight:700;\n color:#4F758B\n \"\n >\n Correo\n </td>\n\n <td\n style=\"\n padding:12px 14px;\n border-bottom:1px solid #D0D0D0;\n font-size:14px\n \"\n >\n ${escapeHtml(requesterEmail)}\n </td>\n </tr>\n\n <tr>\n <td\n style=\"\n padding:12px 14px;\n background:#EEF6E8;\n border-bottom:1px solid #D0D0D0;\n font-size:13px;\n font-weight:700;\n color:#4F758B\n \"\n >\n Aplicación\n </td>\n\n <td\n style=\"\n padding:12px 14px;\n border-bottom:1px solid #D0D0D0;\n font-size:14px;\n font-weight:700;\n color:#4F758B\n \"\n >\n ${escapeHtml(appName)}\n </td>\n </tr>\n\n <tr>\n <td\n style=\"\n padding:12px 14px;\n background:#EEF6E8;\n font-size:13px;\n font-weight:700;\n color:#4F758B\n \"\n >\n Área\n </td>\n\n <td\n style=\"\n padding:12px 14px;\n font-size:14px\n \"\n >\n ${escapeHtml(appCategory)}\n </td>\n </tr>\n\n </table>\n\n </td>\n </tr>\n\n <tr>\n <td style=\"padding:0 34px 30px\">\n\n <p\n style=\"\n margin:0 0 16px;\n font-size:13px;\n line-height:1.55;\n color:#6B8FA3\n \"\n >\n Por seguridad, cada botón abre una pantalla de confirmación.\n La primera decisión confirmada será definitiva para todos los aprobadores.\n </p>\n\n <table\n role=\"presentation\"\n cellspacing=\"0\"\n cellpadding=\"0\"\n >\n <tr>\n\n <td style=\"padding-right:10px\">\n <a\n href=\"${approveUrl}\"\n style=\"\n display:inline-block;\n padding:13px 22px;\n background:#6CC24A;\n color:#FFFFFF;\n text-decoration:none;\n font-size:14px;\n font-weight:700;\n border-radius:3px\n \"\n >\n Aceptar solicitud\n </a>\n </td>\n\n <td>\n <a\n href=\"${rejectUrl}\"\n style=\"\n display:inline-block;\n padding:12px 22px;\n background:#FFFFFF;\n color:#E87722;\n text-decoration:none;\n font-size:14px;\n font-weight:700;\n border:1px solid #E87722;\n border-radius:3px\n \"\n >\n Rechazar solicitud\n </a>\n </td>\n\n </tr>\n </table>\n\n </td>\n </tr>\n\n <tr>\n <td\n style=\"\n padding:18px 34px;\n border-top:1px solid #D0D0D0;\n font-size:11px;\n line-height:1.5;\n color:#6B8FA3\n \"\n >\n GomezLee Marketing · Uso interno\n </td>\n </tr>\n\n </table>\n\n </td>\n </tr>\n</table>\n\n</body>\n</html>`;\n\n return {\n json: {\n created: true,\n requestId,\n sendTo: String(approver.email ?? ''),\n subject: `Solicitud de acceso | ${appName} | ${requesterName}`,\n html\n }\n };\n});"
},
"id": "b38bcf45-4f58-4997-87e2-247c6478bdc2",
"name": "Preparar correos de aprobación",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
448,
768
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "02a636e0-63a0-4a56-b108-65029e555646",
"leftValue": "={{ $json.created }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "d1c724f7-4048-4545-b055-55d355fbe4bc",
"name": "¿Solicitud registrada?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
688,
768
]
},
{
"parameters": {
"sendTo": "={{ $json.sendTo }}",
"subject": "={{ $json.subject }}",
"message": "={{ $json.html }}",
"options": {
"appendAttribution": false,
"senderName": "GLM Hub"
}
},
"id": "b1a5605e-0ad9-4cae-8290-dec2427f645f",
"name": "Enviar solicitud a aprobadores",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
928,
672
],
"webhookId": "7dc65e7b-4fb6-4a2f-b685-34171dd9ce1a",
"credentials": {
"gmailOAuth2": {
"id": "UDcO1FLJqA453V2D",
"name": "Gmail account 3"
}
}
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { ok: true, message: 'La solicitud fue enviada correctamente a las personas responsables de aprobación.', requestId: $('Preparar correos de aprobación').first().json.requestId } }}",
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Cache-Control",
"value": "no-store"
}
]
}
}
},
"id": "8280f9b8-18ee-456c-8e71-2c923f8e60ec",
"name": "Responder solicitud enviada",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
1168,
672
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ {\n ok: false,\n error: $json.publicError || 'No pudimos enviar la solicitud. Intenta nuevamente.'\n} }}",
"options": {
"responseCode": 409,
"responseHeaders": {
"entries": [
{
"name": "Cache-Control",
"value": "no-store"
}
]
}
}
},
"id": "59696c39-a80c-4548-80a8-162d63ea1346",
"name": "Responder error al registrar",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
928,
880
]
},
{
"parameters": {
"path": "glm-hub-confirmar-solicitud",
"responseMode": "responseNode",
"options": {}
},
"id": "e0152ea1-39a8-49e1-add4-ec5f83b7b288",
"name": "Webhook - Confirmar decisión",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-560,
1600
],
"webhookId": "cba1c38c-37bc-4acb-bd61-48035604a5c4"
},
{
"parameters": {
"jsCode": "const query = $json.query ?? {};\nconst token = String(query.token ?? '').trim();\nconst decision = String(query.decision ?? '').trim().toLowerCase();\nconst appName = String(query.app ?? 'aplicación GLM').trim();\nconst valid = /^[0-9a-f]{64}$/i.test(token) && ['approved','rejected'].includes(decision);\nconst escapeHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\nconst actionUrl = 'https://agenteit.digitalcompass.agency/webhook/glm-hub-resolver-solicitud';\nconst approved = decision === 'approved';\nconst title = approved ? 'Confirmar aprobación' : 'Confirmar rechazo';\nconst actionLabel = approved ? 'Sí, aprobar solicitud' : 'Sí, rechazar solicitud';\nconst actionColor = approved ? '#6CC24A' : '#E87722';\nconst html = valid\n ? `<!doctype html><html lang=\"es\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>${title} · GLM Hub</title></head><body style=\"margin:0;background:#F5F5F5;font-family:Arial,sans-serif;color:#4A4A4A\"><main style=\"min-height:100vh;display:grid;place-items:center;padding:24px\"><section style=\"width:100%;max-width:560px;background:#FFFFFF;border-top:4px solid #6CC24A;padding:36px;box-sizing:border-box;box-shadow:0 24px 70px rgba(0,0,0,.14)\"><img src=\"https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM.png\" width=\"220\" alt=\"GomezLee Marketing\" style=\"display:block;width:220px;max-width:100%;height:auto\"><p style=\"margin:28px 0 8px;font-size:11px;font-weight:700;letter-spacing:2px;color:#4F758B\">GLM HUB · DECISIÓN DE ACCESO</p><h1 style=\"margin:0;color:#4F758B;font-size:34px;line-height:1.1\">${title}</h1><p style=\"margin:16px 0 24px;font-size:15px;line-height:1.6\">Vas a ${approved ? 'aprobar' : 'rechazar'} la solicitud de acceso a <strong>${escapeHtml(appName)}</strong>. La decisión será definitiva y los otros enlaces quedarán sin efecto.</p><form method=\"post\" action=\"${actionUrl}\"><input type=\"hidden\" name=\"token\" value=\"${escapeHtml(token)}\"><input type=\"hidden\" name=\"decision\" value=\"${escapeHtml(decision)}\"><button type=\"submit\" style=\"width:100%;min-height:50px;border:0;border-radius:3px;background:${actionColor};color:#FFFFFF;font-family:Arial,sans-serif;font-size:15px;font-weight:700;cursor:pointer\">${actionLabel}</button></form><p style=\"margin:18px 0 0;font-size:12px;line-height:1.5;color:#6B8FA3\">Cierra esta pestaña para cancelar sin registrar ninguna decisión.</p></section></main></body></html>`\n : `<!doctype html><html lang=\"es\"><body style=\"margin:0;background:#F5F5F5;font-family:Arial,sans-serif\"><main style=\"min-height:100vh;display:grid;place-items:center;padding:24px\"><section style=\"max-width:520px;background:#FFFFFF;border-top:4px solid #E87722;padding:36px\"><img src=\"https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM.png\" width=\"220\" alt=\"GomezLee Marketing\"><h1 style=\"color:#4F758B\">Enlace no válido</h1><p style=\"color:#4A4A4A;line-height:1.6\">El enlace está incompleto o no corresponde a una solicitud de GLM Hub.</p></section></main></body></html>`;\nreturn [{ json: { html } }];"
},
"id": "1cafd88f-b081-45fe-ab11-9de66d2337e0",
"name": "Construir página de confirmación",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-336,
1600
]
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $json.html }}",
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/html; charset=utf-8"
},
{
"name": "Cache-Control",
"value": "no-store"
},
{
"name": "X-Frame-Options",
"value": "DENY"
}
]
}
}
},
"id": "7827c03c-dae8-4b9b-9c31-7f2a3a1e7dfe",
"name": "Mostrar confirmación",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-112,
1600
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "glm-hub-resolver-solicitud",
"responseMode": "responseNode",
"options": {}
},
"id": "1cc0149c-206d-479e-b432-d9060bebb79d",
"name": "Webhook - Resolver solicitud",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-1536,
2288
],
"webhookId": "54eeee1b-b24b-46ae-84dc-70c4103e3a3a"
},
{
"parameters": {
"jsCode": "const source = $json.body ?? $json;\nlet payload = source;\nif (typeof payload === 'string') {\n try {\n payload = Object.fromEntries(new URLSearchParams(payload));\n } catch { payload = {}; }\n}\nconst token = String(payload?.token ?? '').trim();\nconst decision = String(payload?.decision ?? '').trim().toLowerCase();\nlet error = '';\nif (!/^[0-9a-f]{64}$/i.test(token)) error = 'El enlace de decisión no es válido.';\nelse if (!['approved','rejected'].includes(decision)) error = 'La decisión recibida no es válida.';\nreturn [{ json: { valid: !error, error, token, decision } }];"
},
"id": "864d54fe-72f0-488a-bff6-0de0d9044308",
"name": "Validar decisión",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1312,
2288
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "5c42c35a-903b-4a18-a7ef-aab6e6bb5c26",
"leftValue": "={{ $json.valid }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "07f79dcf-3d55-4122-9713-f3a1009e4657",
"name": "¿Decisión válida?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-1088,
2288
]
},
{
"parameters": {
"jsCode": "const message = String($json.error ?? 'El enlace de decisión no es válido.');\nconst escapeHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\nconst html = `<!doctype html><html lang=\"es\"><body style=\"margin:0;background:#F5F5F5;font-family:Arial,sans-serif\"><main style=\"min-height:100vh;display:grid;place-items:center;padding:24px\"><section style=\"max-width:520px;background:#FFFFFF;border-top:4px solid #E87722;padding:36px\"><img src=\"https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM.png\" width=\"220\" alt=\"GomezLee Marketing\"><h1 style=\"color:#4F758B\">No se pudo procesar</h1><p style=\"color:#4A4A4A;line-height:1.6\">${escapeHtml(message)}</p></section></main></body></html>`;\nreturn [{ json: { html } }];"
},
"id": "da7d60fb-beb0-4a3e-94e7-31cd643b40c7",
"name": "Preparar decisión inválida",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-864,
2480
]
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $json.html }}",
"options": {
"responseCode": 400,
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/html; charset=utf-8"
},
{
"name": "Cache-Control",
"value": "no-store"
},
{
"name": "X-Frame-Options",
"value": "DENY"
}
]
}
}
},
"id": "1154375e-9680-4582-8bdd-514b15d27726",
"name": "Responder decisión inválida",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-624,
2480
]
},
{
"parameters": {
"method": "POST",
"url": "https://dbit.digitalcompass.agency/rest/v1/rpc/glm_hub_decide_access_request",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Authorization",
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Accept",
"value": "application/json"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\n p_token: $('Validar decisión').first().json.token,\n p_decision: $('Validar decisión').first().json.decision\n} }}",
"options": {
"response": {
"response": {
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "f453b62b-82ef-49e1-8b7a-e50a51a14fa3",
"name": "Registrar decisión en Supabase",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-864,
2208
]
},
{
"parameters": {
"jsCode": "const source = $input.first()?.json ?? {};\nlet payload = source.body ?? source;\nif (typeof payload === 'string') { try { payload = JSON.parse(payload); } catch { payload = {}; } }\nif (Array.isArray(payload)) payload = payload[0] ?? {};\nconst escapeHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\nconst status = String(payload.status ?? '');\nconst approved = status === 'approved';\nconst statusLabel = approved ? 'APROBADA' : status === 'rejected' ? 'RECHAZADA' : 'SIN PROCESAR';\nconst statusColor = approved ? '#6CC24A' : '#E87722';\nconst ok = payload.ok === true;\nconst alreadyDecided = payload.alreadyDecided === true;\nconst requesterName = String(payload.requesterName ?? '');\nconst requesterEmail = String(payload.requesterEmail ?? '');\nconst appName = String(payload.appName ?? 'Aplicación GLM');\nconst decidedByName = String(payload.decidedByName ?? '');\nconst decidedByEmail = String(payload.decidedByEmail ?? '');\nconst responseTitle = ok ? (approved ? 'Solicitud aprobada' : 'Solicitud rechazada') : alreadyDecided ? 'Solicitud ya atendida' : 'No se pudo procesar';\nconst responseMessage = ok\n ? `La decisión fue registrada. IT Support recibirá los datos para continuar con el acceso.`\n : alreadyDecided\n ? `Esta solicitud ya fue ${status === 'approved' ? 'aprobada' : 'rechazada'} por ${escapeHtml(decidedByName || decidedByEmail || 'otra persona')}. No se realizó ningún cambio adicional.`\n : escapeHtml(payload.error ?? 'El enlace no pudo ser validado.');\nconst responseHtml = `<!doctype html><html lang=\"es\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>${responseTitle} · GLM Hub</title></head><body style=\"margin:0;background:#F5F5F5;font-family:Arial,sans-serif;color:#4A4A4A\"><main style=\"min-height:100vh;display:grid;place-items:center;padding:24px\"><section style=\"width:100%;max-width:560px;background:#FFFFFF;border-top:4px solid ${ok ? statusColor : '#4F758B'};padding:36px;box-sizing:border-box;box-shadow:0 24px 70px rgba(0,0,0,.14)\"><img src=\"https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM.png\" width=\"220\" alt=\"GomezLee Marketing\" style=\"display:block;width:220px;max-width:100%;height:auto\"><p style=\"margin:28px 0 8px;font-size:11px;font-weight:700;letter-spacing:2px;color:#4F758B\">GLM HUB · SOLICITUD DE ACCESO</p><h1 style=\"margin:0;color:#4F758B;font-size:34px;line-height:1.1\">${responseTitle}</h1><p style=\"margin:16px 0 0;font-size:15px;line-height:1.65\">${responseMessage}</p><p style=\"margin:24px 0 0;font-size:12px;color:#6B8FA3\">Ya puedes cerrar esta pestaña.</p></section></main></body></html>`;\nconst itHtml = ok ? `<!doctype html><html><body style=\"margin:0;background:#F5F5F5;font-family:Arial,sans-serif;color:#4A4A4A\"><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"background:#F5F5F5\"><tr><td align=\"center\" style=\"padding:32px 16px\"><table role=\"presentation\" width=\"680\" cellspacing=\"0\" cellpadding=\"0\" style=\"width:100%;max-width:680px;background:#FFFFFF;border-collapse:collapse;border-top:4px solid ${statusColor}\"><tr><td style=\"padding:28px 34px 16px\"><img src=\"https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM.png\" width=\"220\" alt=\"GomezLee Marketing\" style=\"display:block;width:220px;max-width:100%;height:auto\"></td></tr><tr><td style=\"padding:8px 34px 0\"><div style=\"font-size:11px;font-weight:700;letter-spacing:2px;color:#4F758B\">GLM HUB · DECISIÓN REGISTRADA</div><h1 style=\"margin:12px 0 8px;font-size:30px;line-height:1.12;color:#4F758B\">Solicitud ${statusLabel.toLowerCase()}.</h1><p style=\"margin:0;font-size:15px;line-height:1.6\">IT Support debe continuar según la decisión indicada.</p></td></tr><tr><td style=\"padding:24px 34px\"><div style=\"display:inline-block;margin-bottom:16px;padding:7px 12px;border:1px solid ${statusColor};color:${statusColor};font-size:12px;font-weight:700;letter-spacing:1px\">${statusLabel}</div><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:collapse;border:1px solid #D0D0D0\"><tr><td style=\"width:38%;padding:12px 14px;background:#EEF6E8;border-bottom:1px solid #D0D0D0;font-size:13px;font-weight:700;color:#4F758B\">Persona solicitante</td><td style=\"padding:12px 14px;border-bottom:1px solid #D0D0D0;font-size:14px\">${escapeHtml(requesterName)}</td></tr><tr><td style=\"padding:12px 14px;background:#EEF6E8;border-bottom:1px solid #D0D0D0;font-size:13px;font-weight:700;color:#4F758B\">Correo solicitante</td><td style=\"padding:12px 14px;border-bottom:1px solid #D0D0D0;font-size:14px\">${escapeHtml(requesterEmail)}</td></tr><tr><td style=\"padding:12px 14px;background:#EEF6E8;border-bottom:1px solid #D0D0D0;font-size:13px;font-weight:700;color:#4F758B\">Aplicación</td><td style=\"padding:12px 14px;border-bottom:1px solid #D0D0D0;font-size:14px;font-weight:700;color:#4F758B\">${escapeHtml(appName)}</td></tr><tr><td style=\"padding:12px 14px;background:#EEF6E8;border-bottom:1px solid #D0D0D0;font-size:13px;font-weight:700;color:#4F758B\">Decisión</td><td style=\"padding:12px 14px;border-bottom:1px solid #D0D0D0;font-size:14px;font-weight:700;color:${statusColor}\">${statusLabel}</td></tr><tr><td style=\"padding:12px 14px;background:#EEF6E8;border-bottom:1px solid #D0D0D0;font-size:13px;font-weight:700;color:#4F758B\">Decidido por</td><td style=\"padding:12px 14px;border-bottom:1px solid #D0D0D0;font-size:14px\">${escapeHtml(decidedByName)}</td></tr><tr><td style=\"padding:12px 14px;background:#EEF6E8;font-size:13px;font-weight:700;color:#4F758B\">Correo de quien decidió</td><td style=\"padding:12px 14px;font-size:14px\">${escapeHtml(decidedByEmail)}</td></tr></table></td></tr><tr><td style=\"padding:18px 34px;border-top:1px solid #D0D0D0;font-size:11px;line-height:1.5;color:#6B8FA3\">GomezLee Marketing · GLM Hub · Uso interno</td></tr></table></td></tr></table></body></html>` : '';\nreturn [{ json: { notifyIt: ok, responseHtml, itHtml, subject: `Acceso ${approved ? 'aprobado' : 'rechazado'} | ${appName} | ${requesterName}` } }];"
},
"id": "6913ff01-1482-4413-964f-eff10fa4a64e",
"name": "Preparar resultado de decisión",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
352,
2224
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "2ef854a5-d270-49bf-9a6a-5bb92aeab434",
"leftValue": "={{ $json.notifyIt }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "343ab147-11df-4030-96b9-b750e6151100",
"name": "¿Notificar a IT Support?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
592,
2224
]
},
{
"parameters": {
"sendTo": "iaracena@gomezleemarketing.com, ethen@gomezleemarketing.com, lmatos@gomezleemarketing.com",
"subject": "={{ $json.subject }}",
"message": "={{ $json.itHtml }}",
"options": {
"appendAttribution": false,
"senderName": "GLM Hub"
}
},
"id": "c1be686a-a5a6-4285-abbd-bf9ed90247d0",
"name": "Enviar decisión a IT Support",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
832,
2128
],
"webhookId": "234c8e3b-b904-41b8-8a74-9ca835f759df",
"credentials": {
"gmailOAuth2": {
"id": "UDcO1FLJqA453V2D",
"name": "Gmail account 3"
}
}
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $('Preparar resultado de decisión').first().json.responseHtml }}",
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/html; charset=utf-8"
},
{
"name": "Cache-Control",
"value": "no-store"
},
{
"name": "X-Frame-Options",
"value": "DENY"
}
]
}
}
},
"id": "f3f941b3-b902-4ed6-abd4-ec3e00baee36",
"name": "Mostrar resultado final",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
1072,
2224
]
},
{
"parameters": {
"jsCode": "const source = $input.first()?.json ?? {};\n\nconst parseJson = (value) => {\n if (typeof value !== 'string') {\n return value;\n }\n\n try {\n return JSON.parse(value);\n } catch {\n return {};\n }\n};\n\nlet payload = source;\n\n// Compatibilidad por si n8n todavía entrega algún envoltorio.\nif (source.body !== undefined) {\n payload = source.body;\n} else if (source.data !== undefined) {\n payload = source.data;\n}\n\npayload = parseJson(payload);\n\nif (Array.isArray(payload)) {\n payload = payload[0] ?? {};\n}\n\nconst registered = payload?.ok === true;\nconst code = String(payload?.code ?? '').toUpperCase();\nconst internalMessage = String(\n payload?.error ??\n payload?.message ??\n ''\n).trim();\n\nconst normalizedMessage = internalMessage.toLowerCase();\n\nlet publicError = 'No pudimos enviar la solicitud. Intenta nuevamente.';\n\nif (\n code === 'PENDING_EXISTS' ||\n normalizedMessage.includes('solicitud pendiente')\n) {\n publicError = 'Ya tienes una solicitud pendiente para esta aplicación.';\n} else if (\n normalizedMessage.includes('sesión') ||\n normalizedMessage.includes('jwt')\n) {\n publicError = 'Tu sesión venció. Cierra sesión e inicia nuevamente.';\n} else if (\n normalizedMessage.includes('administradores no necesitan')\n) {\n publicError =\n 'Tu cuenta administrativa no necesita solicitar acceso desde el Hub.';\n} else if (\n normalizedMessage.includes('no está autorizado')\n) {\n publicError =\n 'Tu cuenta no está habilitada para enviar solicitudes de acceso.';\n} else if (\n normalizedMessage.includes('ya no está publicada')\n) {\n publicError =\n 'La aplicación seleccionada ya no está disponible.';\n}\n\nreturn [\n {\n json: {\n registered,\n publicError,\n internalMessage,\n requestId: String(payload?.requestId ?? ''),\n requesterName: String(payload?.requesterName ?? ''),\n requesterEmail: String(payload?.requesterEmail ?? ''),\n appName: String(payload?.appName ?? ''),\n appCategory: String(payload?.appCategory ?? '')\n }\n }\n];"
},
"id": "5e121030-2188-415d-8013-a54d58fedc91",
"name": "Confirmar registro de solicitud",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-480,
816
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "aa091e1c-09f8-4cdb-b8ac-3cbc4a7a33c8",
"leftValue": "={{ $json.registered }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "f722416a-53e0-403d-a301-8bfe4d43de60",
"name": "¿Solicitud registrada en Supabase?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-256,
816
]
},
{
"parameters": {
"method": "POST",
"url": "https://dbit.digitalcompass.agency/rest/v1/rpc/glm_hub_issue_access_request_tokens",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Authorization",
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q"
},
{
"name": "Accept",
"value": "application/json"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\n p_request_id: $('Confirmar registro de solicitud').first().json.requestId\n} }}",
"options": {
"response": {
"response": {
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "dfe96316-ab43-49e2-b9fb-384d34c5389c",
"name": "Emitir enlaces de aprobación",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
208,
768
]
},
{
"parameters": {
"content": "## 1. RECEPCIÓN Y VALIDACIÓN\n\nEste bloque recibe desde GLM Hub una nueva solicitud de acceso y valida que la información obligatoria sea correcta.\n\n- Verifica los datos del usuario y de la aplicación solicitada.\n- Rechaza inmediatamente solicitudes incompletas o inválidas.\n- Solo las solicitudes válidas continúan hacia el registro en Supabase.",
"height": 560,
"width": 1104,
"color": 6
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-2016,
688
],
"id": "97451dbc-cd46-40e8-911b-7c4cf3e648f4",
"name": "Sticky Note"
},
{
"parameters": {
"content": "## 2. REGISTRO DE LA SOLICITUD\n\nGuarda la solicitud de acceso en Supabase y confirma que el registro se haya creado correctamente.\n\n- Almacena la información de la solicitud y su estado inicial.\n- Recupera y normaliza los datos devueltos por Supabase.\n- Solo permite continuar cuando existe una confirmación válida del registro.",
"height": 384,
"width": 736,
"color": 5
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-800,
640
],
"id": "5f36aafe-12ce-431d-90e8-2b49cf214f1d",
"name": "Sticky Note1"
},
{
"parameters": {
"content": "## 3. ENVÍO A LOS APROBADORES\n\nGenera los enlaces de decisión, prepara los correos y notifica a las personas responsables de aprobar o rechazar la solicitud.\n\n- Mantiene como aprobadores base a Isaac Aracena, José Leopoldo Gómez y Máximo Gómez.\n- Agrega automáticamente al jefe inmediato del solicitante cuando existe `supervisor_email` en `empleados_glm`.\n- Evita duplicados cuando el jefe ya es uno de los aprobadores base.\n- Construye enlaces únicos para la aprobación y el rechazo.\n- Envía un correo personalizado a cada aprobador mediante Gmail.\n- La primera decisión registrada continúa siendo definitiva para todos los enlaces.",
"height": 560,
"width": 1264,
"color": "#357C27"
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
112,
480
],
"id": "bab6dbac-ae4b-46ee-b31e-e894aa1fc20a",
"name": "Sticky Note2"
},
{
"parameters": {
"content": "## 4. CONFIRMACIÓN DE LA DECISIÓN\n\nRecibe al aprobador desde el enlace enviado por correo y muestra una página previa antes de registrar la decisión definitiva.\n\n- Lee los parámetros incluidos en el enlace de aprobación o rechazo.\n- Construye una página con los datos de la solicitud.\n- Solicita una confirmación final para evitar decisiones accidentales.",
"height": 352,
"width": 1062,
"color": "#8F622D"
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-800,
1424
],
"id": "9115ee9e-a2ce-419f-a783-62cfe268c8fd",
"name": "Sticky Note3"
},
{
"parameters": {
"content": "## 5. VALIDACIÓN Y REGISTRO DE LA DECISIÓN\n\nValida la decisión confirmada por el aprobador y actualiza la solicitud correspondiente en Supabase.\n\n- Comprueba que la solicitud, el enlace y la decisión sean válidos.\n- Registra en Supabase la aprobación o el rechazo.\n- Impide procesar decisiones incorrectas, incompletas o manipuladas.\n- Muestra una respuesta controlada cuando la decisión no puede ser aceptada.",
"height": 624,
"width": 1184,
"color": "#52500F"
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-1616,
2032
],
"id": "68fac86e-bbee-4648-b2ed-bfe3b9e9829c",
"name": "Sticky Note4"
},
{
"parameters": {
"content": "## 6. NOTIFICACIÓN Y RESULTADO FINAL\n\nPrepara el resultado de la decisión, determina si se necesita intervención de IT Support y finaliza el proceso.\n\n- Construye el mensaje final de aprobación o rechazo.\n- Evalúa si la decisión debe notificarse al equipo de IT.\n- Envía por Gmail la información necesaria cuando corresponde.\n- Muestra al aprobador el resultado definitivo de la operación.",
"height": 432,
"width": 1104,
"color": "#276D4F"
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
192,
1968
],
"id": "024b61e1-bbc8-4838-9afe-7ee36445d0db",
"name": "Sticky Note5"
}
],
"connections": {
"Webhook - Solicitar acceso": {
"main": [
[
{
"node": "Validar solicitud de acceso",
"type": "main",
"index": 0
}
]
]
},
"Validar solicitud de acceso": {
"main": [
[
{
"node": "¿Solicitud de acceso válida?",
"type": "main",
"index": 0
}
]
]
},
"¿Solicitud de acceso válida?": {
"main": [
[
{
"node": "Crear solicitud en Supabase",
"type": "main",
"index": 0
}
],
[
{
"node": "Responder solicitud inválida",
"type": "main",
"index": 0
}
]
]
},
"Crear solicitud en Supabase": {
"main": [
[
{
"node": "Confirmar registro de solicitud",
"type": "main",
"index": 0
}
]
]
},
"Preparar correos de aprobación": {
"main": [
[
{
"node": "¿Solicitud registrada?",
"type": "main",
"index": 0
}
]
]
},
"¿Solicitud registrada?": {
"main": [
[
{
"node": "Enviar solicitud a aprobadores",
"type": "main",
"index": 0
}
],
[
{
"node": "Responder error al registrar",
"type": "main",
"index": 0
}
]
]
},
"Enviar solicitud a aprobadores": {
"main": [
[
{
"node": "Responder solicitud enviada",
"type": "main",
"index": 0
}
]
]
},
"Webhook - Confirmar decisión": {
"main": [
[
{
"node": "Construir página de confirmación",
"type": "main",
"index": 0
}
]
]
},
"Construir página de confirmación": {
"main": [
[
{
"node": "Mostrar confirmación",
"type": "main",
"index": 0
}
]
]
},
"Webhook - Resolver solicitud": {
"main": [
[
{
"node": "Validar decisión",
"type": "main",
"index": 0
}
]
]
},
"Validar decisión": {
"main": [
[
{
"node": "¿Decisión válida?",
"type": "main",
"index": 0
}
]
]
},
"¿Decisión válida?": {
"main": [
[
{
"node": "Registrar decisión en Supabase",
"type": "main",
"index": 0
}
],
[
{
"node": "Preparar decisión inválida",
"type": "main",
"index": 0
}
]
]
},
"Preparar decisión inválida": {
"main": [
[
{
"node": "Responder decisión inválida",
"type": "main",
"index": 0
}
]
]
},
"Registrar decisión en Supabase": {
"main": [
[
{
"node": "Preparar resultado de decisión",
"type": "main",
"index": 0
}
]
]
},
"Preparar resultado de decisión": {
"main": [
[
{
"node": "¿Notificar a IT Support?",
"type": "main",
"index": 0
}
]
]
},
"¿Notificar a IT Support?": {
"main": [
[
{
"node": "Enviar decisión a IT Support",
"type": "main",
"index": 0
}
],
[
{
"node": "Mostrar resultado final",
"type": "main",
"index": 0
}
]
]
},
"Enviar decisión a IT Support": {
"main": [
[
{
"node": "Mostrar resultado final",
"type": "main",
"index": 0
}
]
]
},
"Confirmar registro de solicitud": {
"main": [
[
{
"node": "¿Solicitud registrada en Supabase?",
"type": "main",
"index": 0
}
]
]
},
"¿Solicitud registrada en Supabase?": {
"main": [
[
{
"node": "Emitir enlaces de aprobación",
"type": "main",
"index": 0
}
],
[
{
"node": "Responder error al registrar",
"type": "main",
"index": 0
}
]
]
},
"Emitir enlaces de aprobación": {
"main": [
[
{
"node": "Preparar correos de aprobación",
"type": "main",
"index": 0
}
]
]
}
},
"authors": "Isaac Aracena",
"name": "Version 9b79c73b",
"description": "",
"autosaved": true,
"workflowPublishHistory": [
{
"createdAt": "2026-08-18T13:05:10.470Z",
"id": 4868,
"workflowId": "RaNkDfE2YCrZgCbx",
"versionId": "9b79c73b-73e6-43ea-bf12-90c30f7fb7c1",
"event": "activated",
"userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029"
}
]
}
}