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

1932 lines
88 KiB
JSON

{
"updatedAt": "2026-07-30T22:15:55.410Z",
"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": "80c8fbc9-c126-447e-a2b3-b2c9ac6e9468",
"name": "Webhook - Solicitar acceso",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-784,
208
],
"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": "7d5b3911-3db7-4023-a89b-ac690bc99101",
"name": "Validar solicitud de acceso",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-560,
208
]
},
{
"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": "3c1cd649-6e3a-4751-ba1f-e775e8040f1d",
"name": "¿Solicitud de acceso válida?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-336,
208
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { ok: false, error: $json.error } }}",
"options": {
"responseCode": 400,
"responseHeaders": {
"entries": [
{
"name": "Cache-Control",
"value": "no-store"
}
]
}
}
},
"id": "ff32cfc2-c5ed-4e10-8804-2209cd17ef69",
"name": "Responder solicitud inválida",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-112,
368
]
},
{
"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": "260ee2e0-d0d7-4395-a186-b6cec2a766db",
"name": "Crear solicitud en Supabase",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-112,
128
]
},
{
"parameters": {
"jsCode": "const source = $input.first()?.json ?? {};\nconst parseJson = (value) => {\n if (typeof value !== 'string') return value;\n try { return JSON.parse(value); } catch { return value; }\n};\nlet payload = source.body ?? source.data ?? source.response?.body ?? source;\npayload = parseJson(payload);\nif (Array.isArray(payload)) payload = payload[0] ?? {};\nif (payload && typeof payload === 'object' && payload.body !== undefined && payload.ok === undefined) {\n payload = parseJson(payload.body);\n if (Array.isArray(payload)) payload = payload[0] ?? {};\n}\nif (payload?.ok !== true) {\n return [{ json: {\n created: false,\n publicError: 'La solicitud fue registrada, pero no se pudo enviar el correo. Intenta nuevamente en unos minutos.',\n technicalError: String(payload?.error ?? payload?.message ?? 'No se pudieron emitir los enlaces de aprobación.')\n } }];\n}\nconst escapeHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\nconst encode = encodeURIComponent;\nconst confirmBase = 'https://agenteit.digitalcompass.agency/webhook/glm-hub-confirmar-solicitud';\nconst requesterName = String(payload.requesterName ?? 'Usuario GLM');\nconst requesterEmail = String(payload.requesterEmail ?? '');\nconst appName = String(payload.appName ?? 'Aplicación GLM');\nconst appCategory = String(payload.appCategory ?? '');\nconst requestId = String(payload.requestId ?? '');\nconst approvers = Array.isArray(payload.approvers) ? payload.approvers : [];\nif (approvers.length !== 3) {\n return [{ json: {\n created: false,\n publicError: 'No se pudo completar el envío de la solicitud. Intenta nuevamente en unos minutos.',\n technicalError: `Se esperaban 3 aprobadores y se recibieron ${approvers.length}.`\n } }];\n}\nreturn approvers.map((approver) => {\n const approveUrl = `${confirmBase}?token=${encode(String(approver.token ?? ''))}&decision=approved&app=${encode(appName)}`;\n const rejectUrl = `${confirmBase}?token=${encode(String(approver.token ?? ''))}&decision=rejected&app=${encode(appName)}`;\n const html = `<!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=\"640\" cellspacing=\"0\" cellpadding=\"0\" style=\"width:100%;max-width:640px;background:#FFFFFF;border-collapse:collapse;border-top:4px solid #6CC24A\"><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 · SOLICITUD DE ACCESO</div><h1 style=\"margin:12px 0 8px;font-size:30px;line-height:1.12;color:#4F758B\">Una persona necesita acceso.</h1><p style=\"margin:0;font-size:15px;line-height:1.6;color:#4A4A4A\">Hola ${escapeHtml(approver.name)}, revisa la siguiente solicitud.</p></td></tr><tr><td style=\"padding:24px 34px\"><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:collapse;border:1px solid #D0D0D0\"><tr><td style=\"width:34%;padding:12px 14px;background:#EEF6E8;border-bottom:1px solid #D0D0D0;font-size:13px;font-weight:700;color:#4F758B\">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</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;font-size:13px;font-weight:700;color:#4F758B\">Área</td><td style=\"padding:12px 14px;font-size:14px\">${escapeHtml(appCategory)}</td></tr></table></td></tr><tr><td style=\"padding:0 34px 30px\"><p style=\"margin:0 0 16px;font-size:13px;line-height:1.55;color:#6B8FA3\">Por seguridad, cada botón abre una pantalla de confirmación. La primera decisión confirmada será definitiva para los tres destinatarios.</p><table role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\"><tr><td style=\"padding-right:10px\"><a href=\"${approveUrl}\" style=\"display:inline-block;padding:13px 22px;background:#6CC24A;color:#FFFFFF;text-decoration:none;font-size:14px;font-weight:700;border-radius:3px\">Aceptar solicitud</a></td><td><a href=\"${rejectUrl}\" style=\"display:inline-block;padding:12px 22px;background:#FFFFFF;color:#E87722;text-decoration:none;font-size:14px;font-weight:700;border:1px solid #E87722;border-radius:3px\">Rechazar solicitud</a></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\">Solicitud ${escapeHtml(requestId)} · GomezLee Marketing · Uso interno</td></tr></table></td></tr></table></body></html>`;\n return { json: { created: true, requestId, sendTo: String(approver.email ?? ''), subject: `Solicitud de acceso | ${appName} | ${requesterName}`, html } };\n});"
},
"id": "c4bd59ac-3233-4d09-81af-17f08b9325f5",
"name": "Preparar correos de aprobación",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
784,
48
]
},
{
"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": "eae9bf96-c598-42e0-b4df-c58319e3c354",
"name": "¿Solicitud registrada?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1024,
48
]
},
{
"parameters": {
"sendTo": "={{ $json.sendTo }}",
"subject": "={{ $json.subject }}",
"message": "={{ $json.html }}",
"options": {
"appendAttribution": false,
"senderName": "GLM Hub"
}
},
"id": "f3f7f82d-d96e-499f-a2d6-9e26f5496bb1",
"name": "Enviar solicitud a aprobadores",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
1264,
-48
],
"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 a Isaac Aracena, José Leopoldo Gómez y Máximo Gómez.', requestId: $('Preparar correos de aprobación').first().json.requestId } }}",
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Cache-Control",
"value": "no-store"
}
]
}
}
},
"id": "af925eb2-9fb8-4261-8652-4e162969e512",
"name": "Responder solicitud enviada",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
1504,
-48
]
},
{
"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": "0a104b37-5336-44ce-953b-979ce5faaa87",
"name": "Responder error al registrar",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
1264,
160
]
},
{
"parameters": {
"path": "glm-hub-confirmar-solicitud",
"responseMode": "responseNode",
"options": {}
},
"id": "569dfd6e-b94f-4db8-8c88-4a890cff1ea3",
"name": "Webhook - Confirmar decisión",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-784,
672
],
"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": "66f986d2-7c8b-4c25-9a5c-4310f5c37d3a",
"name": "Construir página de confirmación",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-560,
672
]
},
{
"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": "abba0ac3-e1fc-4c2a-a6fc-ecba5e924cdf",
"name": "Mostrar confirmación",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-336,
672
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "glm-hub-resolver-solicitud",
"responseMode": "responseNode",
"options": {}
},
"id": "9baccfdb-9561-4e85-bf43-7993eb6bc65d",
"name": "Webhook - Resolver solicitud",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-784,
1040
],
"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": "0760c143-2039-428e-85d2-f9fdec1ed4f7",
"name": "Validar decisión",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-560,
1040
]
},
{
"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": "dfacee52-d608-4086-b287-560ce870c980",
"name": "¿Decisión válida?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-336,
1040
]
},
{
"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": "8bebdbfd-3ee5-4a87-a0d4-c0b3a9bd17d4",
"name": "Preparar decisión inválida",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-112,
1232
]
},
{
"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": "bdaa54d2-e20d-4523-963b-1cc88f4d8669",
"name": "Responder decisión inválida",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
128,
1232
]
},
{
"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": "df9c350b-10f7-494a-8c5e-e0354c1606db",
"name": "Registrar decisión en Supabase",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-112,
960
]
},
{
"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": "72ee2fcd-bf95-4542-aad1-90ca4d83a2e7",
"name": "Preparar resultado de decisión",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
128,
960
]
},
{
"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": "9f4e6707-3cb1-4c0e-833c-05be87602e07",
"name": "¿Notificar a IT Support?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
368,
960
]
},
{
"parameters": {
"sendTo": "itsupport@gomezleemarketing.com",
"subject": "={{ $json.subject }}",
"message": "={{ $json.itHtml }}",
"options": {
"appendAttribution": false,
"senderName": "GLM Hub"
}
},
"id": "56d73834-fc03-43c7-9844-dd4e691439b2",
"name": "Enviar decisión a IT Support",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
608,
864
],
"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": "e1d4a5ba-34fe-479a-a670-e07ebd87d0cf",
"name": "Mostrar resultado final",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
848,
960
]
},
{
"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": "e2cc29db-5db2-46ea-a388-054c320ecfc2",
"name": "Confirmar registro de solicitud",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
96,
128
]
},
{
"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": "7f6803be-bc3a-45e1-b1a4-c04c33ca25b9",
"name": "¿Solicitud registrada en Supabase?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
320,
128
]
},
{
"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": "d684c268-bbd8-4a3b-8aab-b21510423c95",
"name": "Emitir enlaces de aprobación",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
544,
48
]
}
],
"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": "a4528c24-f544-4063-9bda-2b04c9943a97",
"activeVersionId": "a4528c24-f544-4063-9bda-2b04c9943a97",
"versionCounter": 72,
"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-07-29T15:29:25.000Z",
"createdAt": "2026-07-29T15:29:15.992Z",
"versionId": "a4528c24-f544-4063-9bda-2b04c9943a97",
"workflowId": "RaNkDfE2YCrZgCbx",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "glm-hub-solicitar-acceso",
"responseMode": "responseNode",
"options": {
"allowedOrigins": "*"
}
},
"id": "80c8fbc9-c126-447e-a2b3-b2c9ac6e9468",
"name": "Webhook - Solicitar acceso",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-784,
208
],
"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": "7d5b3911-3db7-4023-a89b-ac690bc99101",
"name": "Validar solicitud de acceso",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-560,
208
]
},
{
"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": "3c1cd649-6e3a-4751-ba1f-e775e8040f1d",
"name": "¿Solicitud de acceso válida?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-336,
208
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { ok: false, error: $json.error } }}",
"options": {
"responseCode": 400,
"responseHeaders": {
"entries": [
{
"name": "Cache-Control",
"value": "no-store"
}
]
}
}
},
"id": "ff32cfc2-c5ed-4e10-8804-2209cd17ef69",
"name": "Responder solicitud inválida",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-112,
368
]
},
{
"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": "260ee2e0-d0d7-4395-a186-b6cec2a766db",
"name": "Crear solicitud en Supabase",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-112,
128
]
},
{
"parameters": {
"jsCode": "const source = $input.first()?.json ?? {};\nconst parseJson = (value) => {\n if (typeof value !== 'string') return value;\n try { return JSON.parse(value); } catch { return value; }\n};\nlet payload = source.body ?? source.data ?? source.response?.body ?? source;\npayload = parseJson(payload);\nif (Array.isArray(payload)) payload = payload[0] ?? {};\nif (payload && typeof payload === 'object' && payload.body !== undefined && payload.ok === undefined) {\n payload = parseJson(payload.body);\n if (Array.isArray(payload)) payload = payload[0] ?? {};\n}\nif (payload?.ok !== true) {\n return [{ json: {\n created: false,\n publicError: 'La solicitud fue registrada, pero no se pudo enviar el correo. Intenta nuevamente en unos minutos.',\n technicalError: String(payload?.error ?? payload?.message ?? 'No se pudieron emitir los enlaces de aprobación.')\n } }];\n}\nconst escapeHtml = (value) => String(value ?? '').replace(/[&<>\"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\nconst encode = encodeURIComponent;\nconst confirmBase = 'https://agenteit.digitalcompass.agency/webhook/glm-hub-confirmar-solicitud';\nconst requesterName = String(payload.requesterName ?? 'Usuario GLM');\nconst requesterEmail = String(payload.requesterEmail ?? '');\nconst appName = String(payload.appName ?? 'Aplicación GLM');\nconst appCategory = String(payload.appCategory ?? '');\nconst requestId = String(payload.requestId ?? '');\nconst approvers = Array.isArray(payload.approvers) ? payload.approvers : [];\nif (approvers.length !== 3) {\n return [{ json: {\n created: false,\n publicError: 'No se pudo completar el envío de la solicitud. Intenta nuevamente en unos minutos.',\n technicalError: `Se esperaban 3 aprobadores y se recibieron ${approvers.length}.`\n } }];\n}\nreturn approvers.map((approver) => {\n const approveUrl = `${confirmBase}?token=${encode(String(approver.token ?? ''))}&decision=approved&app=${encode(appName)}`;\n const rejectUrl = `${confirmBase}?token=${encode(String(approver.token ?? ''))}&decision=rejected&app=${encode(appName)}`;\n const html = `<!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=\"640\" cellspacing=\"0\" cellpadding=\"0\" style=\"width:100%;max-width:640px;background:#FFFFFF;border-collapse:collapse;border-top:4px solid #6CC24A\"><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 · SOLICITUD DE ACCESO</div><h1 style=\"margin:12px 0 8px;font-size:30px;line-height:1.12;color:#4F758B\">Una persona necesita acceso.</h1><p style=\"margin:0;font-size:15px;line-height:1.6;color:#4A4A4A\">Hola ${escapeHtml(approver.name)}, revisa la siguiente solicitud.</p></td></tr><tr><td style=\"padding:24px 34px\"><table role=\"presentation\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" style=\"border-collapse:collapse;border:1px solid #D0D0D0\"><tr><td style=\"width:34%;padding:12px 14px;background:#EEF6E8;border-bottom:1px solid #D0D0D0;font-size:13px;font-weight:700;color:#4F758B\">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</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;font-size:13px;font-weight:700;color:#4F758B\">Área</td><td style=\"padding:12px 14px;font-size:14px\">${escapeHtml(appCategory)}</td></tr></table></td></tr><tr><td style=\"padding:0 34px 30px\"><p style=\"margin:0 0 16px;font-size:13px;line-height:1.55;color:#6B8FA3\">Por seguridad, cada botón abre una pantalla de confirmación. La primera decisión confirmada será definitiva para los tres destinatarios.</p><table role=\"presentation\" cellspacing=\"0\" cellpadding=\"0\"><tr><td style=\"padding-right:10px\"><a href=\"${approveUrl}\" style=\"display:inline-block;padding:13px 22px;background:#6CC24A;color:#FFFFFF;text-decoration:none;font-size:14px;font-weight:700;border-radius:3px\">Aceptar solicitud</a></td><td><a href=\"${rejectUrl}\" style=\"display:inline-block;padding:12px 22px;background:#FFFFFF;color:#E87722;text-decoration:none;font-size:14px;font-weight:700;border:1px solid #E87722;border-radius:3px\">Rechazar solicitud</a></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\">Solicitud ${escapeHtml(requestId)} · GomezLee Marketing · Uso interno</td></tr></table></td></tr></table></body></html>`;\n return { json: { created: true, requestId, sendTo: String(approver.email ?? ''), subject: `Solicitud de acceso | ${appName} | ${requesterName}`, html } };\n});"
},
"id": "c4bd59ac-3233-4d09-81af-17f08b9325f5",
"name": "Preparar correos de aprobación",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
784,
48
]
},
{
"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": "eae9bf96-c598-42e0-b4df-c58319e3c354",
"name": "¿Solicitud registrada?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1024,
48
]
},
{
"parameters": {
"sendTo": "={{ $json.sendTo }}",
"subject": "={{ $json.subject }}",
"message": "={{ $json.html }}",
"options": {
"appendAttribution": false,
"senderName": "GLM Hub"
}
},
"id": "f3f7f82d-d96e-499f-a2d6-9e26f5496bb1",
"name": "Enviar solicitud a aprobadores",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
1264,
-48
],
"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 a Isaac Aracena, José Leopoldo Gómez y Máximo Gómez.', requestId: $('Preparar correos de aprobación').first().json.requestId } }}",
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Cache-Control",
"value": "no-store"
}
]
}
}
},
"id": "af925eb2-9fb8-4261-8652-4e162969e512",
"name": "Responder solicitud enviada",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
1504,
-48
]
},
{
"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": "0a104b37-5336-44ce-953b-979ce5faaa87",
"name": "Responder error al registrar",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
1264,
160
]
},
{
"parameters": {
"path": "glm-hub-confirmar-solicitud",
"responseMode": "responseNode",
"options": {}
},
"id": "569dfd6e-b94f-4db8-8c88-4a890cff1ea3",
"name": "Webhook - Confirmar decisión",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-784,
672
],
"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": "66f986d2-7c8b-4c25-9a5c-4310f5c37d3a",
"name": "Construir página de confirmación",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-560,
672
]
},
{
"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": "abba0ac3-e1fc-4c2a-a6fc-ecba5e924cdf",
"name": "Mostrar confirmación",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-336,
672
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "glm-hub-resolver-solicitud",
"responseMode": "responseNode",
"options": {}
},
"id": "9baccfdb-9561-4e85-bf43-7993eb6bc65d",
"name": "Webhook - Resolver solicitud",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-784,
1040
],
"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": "0760c143-2039-428e-85d2-f9fdec1ed4f7",
"name": "Validar decisión",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-560,
1040
]
},
{
"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": "dfacee52-d608-4086-b287-560ce870c980",
"name": "¿Decisión válida?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-336,
1040
]
},
{
"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": "8bebdbfd-3ee5-4a87-a0d4-c0b3a9bd17d4",
"name": "Preparar decisión inválida",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-112,
1232
]
},
{
"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": "bdaa54d2-e20d-4523-963b-1cc88f4d8669",
"name": "Responder decisión inválida",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
128,
1232
]
},
{
"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": "df9c350b-10f7-494a-8c5e-e0354c1606db",
"name": "Registrar decisión en Supabase",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-112,
960
]
},
{
"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": "72ee2fcd-bf95-4542-aad1-90ca4d83a2e7",
"name": "Preparar resultado de decisión",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
128,
960
]
},
{
"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": "9f4e6707-3cb1-4c0e-833c-05be87602e07",
"name": "¿Notificar a IT Support?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
368,
960
]
},
{
"parameters": {
"sendTo": "itsupport@gomezleemarketing.com",
"subject": "={{ $json.subject }}",
"message": "={{ $json.itHtml }}",
"options": {
"appendAttribution": false,
"senderName": "GLM Hub"
}
},
"id": "56d73834-fc03-43c7-9844-dd4e691439b2",
"name": "Enviar decisión a IT Support",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
608,
864
],
"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": "e1d4a5ba-34fe-479a-a670-e07ebd87d0cf",
"name": "Mostrar resultado final",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
848,
960
]
},
{
"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": "e2cc29db-5db2-46ea-a388-054c320ecfc2",
"name": "Confirmar registro de solicitud",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
96,
128
]
},
{
"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": "7f6803be-bc3a-45e1-b1a4-c04c33ca25b9",
"name": "¿Solicitud registrada en Supabase?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
320,
128
]
},
{
"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": "d684c268-bbd8-4a3b-8aab-b21510423c95",
"name": "Emitir enlaces de aprobación",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
544,
48
]
}
],
"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 a4528c24",
"description": "",
"autosaved": true,
"workflowPublishHistory": [
{
"createdAt": "2026-07-29T15:29:25.705Z",
"id": 3624,
"workflowId": "RaNkDfE2YCrZgCbx",
"versionId": "a4528c24-f544-4063-9bda-2b04c9943a97",
"event": "activated",
"userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029"
}
]
}
}