diff --git a/glm-hub-solicitudes-de-acceso-y-aprobacion.json b/glm-hub-solicitudes-de-acceso-y-aprobacion.json index 26a2603..4755b84 100644 --- a/glm-hub-solicitudes-de-acceso-y-aprobacion.json +++ b/glm-hub-solicitudes-de-acceso-y-aprobacion.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-08-06T17:47:19.077Z", + "updatedAt": "2026-08-15T13:31:33.469Z", "createdAt": "2026-07-29T13:08:05.444Z", "id": "RaNkDfE2YCrZgCbx", "name": "GLM Hub - Solicitudes de acceso y aprobación", @@ -16,13 +16,13 @@ "allowedOrigins": "*" } }, - "id": "80c8fbc9-c126-447e-a2b3-b2c9ac6e9468", + "id": "6fee8ce5-2837-4683-9b9c-fd1022a87f5c", "name": "Webhook - Solicitar acceso", "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ - -928, - 192 + -1920, + 896 ], "webhookId": "ad3e98b9-baf2-400f-90e7-dcd4c5c20457" }, @@ -30,13 +30,13 @@ "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", + "id": "05811eaf-72a8-448c-8c01-16b9d9d7a592", "name": "Validar solicitud de acceso", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - -704, - 192 + -1696, + 896 ] }, { @@ -64,13 +64,13 @@ }, "options": {} }, - "id": "3c1cd649-6e3a-4751-ba1f-e775e8040f1d", + "id": "fddbe870-eea1-4b94-9e64-c4aec9c4dad7", "name": "¿Solicitud de acceso válida?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ - -480, - 192 + -1472, + 896 ] }, { @@ -89,13 +89,13 @@ } } }, - "id": "ff32cfc2-c5ed-4e10-8804-2209cd17ef69", + "id": "397ca441-991c-4f5d-9ce8-1df22a9fccc8", "name": "Responder solicitud inválida", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ - -160, - 352 + -1152, + 1056 ] }, { @@ -132,26 +132,26 @@ "timeout": 30000 } }, - "id": "260ee2e0-d0d7-4395-a186-b6cec2a766db", + "id": "1315636b-e124-4a59-9308-61f6855c482a", "name": "Crear solicitud en Supabase", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 304, - 112 + -688, + 816 ] }, { "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) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[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 = `
\"GomezLee
GLM HUB · SOLICITUD DE ACCESO

Una persona necesita acceso.

Hola ${escapeHtml(approver.name)}, revisa la siguiente solicitud.

Solicitante${escapeHtml(requesterName)}
Correo${escapeHtml(requesterEmail)}
Aplicación${escapeHtml(appName)}
Área${escapeHtml(appCategory)}

Por seguridad, cada botón abre una pantalla de confirmación. La primera decisión confirmada será definitiva para los tres destinatarios.

Aceptar solicitudRechazar solicitud
Solicitud ${escapeHtml(requestId)} · GomezLee Marketing · Uso interno
`;\n return { json: { created: true, requestId, sendTo: String(approver.email ?? ''), subject: `Solicitud de acceso | ${appName} | ${requesterName}`, html } };\n});" + "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 '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\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\n\n\n\n\n \n \n\n \n\n \n \n \n \n \n\n \n \n\n \n GLM HUB · SOLICITUD DE ACCESO\n \n\n \n Una persona necesita acceso.\n \n\n \n Hola ${escapeHtml(approver.name)}, revisa la siguiente solicitud.\n

\n\n \n \n\n \n \n\n \n\n \n \n Solicitante\n \n\n \n ${escapeHtml(requesterName)}\n \n \n\n \n \n Correo\n \n\n \n ${escapeHtml(requesterEmail)}\n \n \n\n \n \n Aplicación\n \n\n \n ${escapeHtml(appName)}\n \n \n\n \n \n Área\n \n\n \n ${escapeHtml(appCategory)}\n \n \n\n \n\n \n \n\n \n \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

\n\n \n \n\n \n \n Aceptar solicitud\n \n \n\n \n \n Rechazar solicitud\n \n \n\n \n \n\n \n \n\n \n \n GomezLee Marketing · Uso interno\n \n \n\n \n\n \n \n\n\n\n`;\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": "c4bd59ac-3233-4d09-81af-17f08b9325f5", + "id": "b38bcf45-4f58-4997-87e2-247c6478bdc2", "name": "Preparar correos de aprobación", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 1440, - 64 + 448, + 768 ] }, { @@ -179,13 +179,13 @@ }, "options": {} }, - "id": "eae9bf96-c598-42e0-b4df-c58319e3c354", + "id": "d1c724f7-4048-4545-b055-55d355fbe4bc", "name": "¿Solicitud registrada?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ - 1680, - 64 + 688, + 768 ] }, { @@ -198,13 +198,13 @@ "senderName": "GLM Hub" } }, - "id": "f3f7f82d-d96e-499f-a2d6-9e26f5496bb1", + "id": "b1a5605e-0ad9-4cae-8290-dec2427f645f", "name": "Enviar solicitud a aprobadores", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [ - 1920, - -32 + 928, + 672 ], "webhookId": "7dc65e7b-4fb6-4a2f-b685-34171dd9ce1a", "credentials": { @@ -217,7 +217,7 @@ { "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 } }}", + "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": { @@ -230,13 +230,13 @@ } } }, - "id": "af925eb2-9fb8-4261-8652-4e162969e512", + "id": "8280f9b8-18ee-456c-8e71-2c923f8e60ec", "name": "Responder solicitud enviada", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ - 2160, - -32 + 1168, + 672 ] }, { @@ -255,13 +255,13 @@ } } }, - "id": "0a104b37-5336-44ce-953b-979ce5faaa87", + "id": "59696c39-a80c-4548-80a8-162d63ea1346", "name": "Responder error al registrar", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ - 1920, - 176 + 928, + 880 ] }, { @@ -270,13 +270,13 @@ "responseMode": "responseNode", "options": {} }, - "id": "569dfd6e-b94f-4db8-8c88-4a890cff1ea3", + "id": "e0152ea1-39a8-49e1-add4-ec5f83b7b288", "name": "Webhook - Confirmar decisión", "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ - 432, - 896 + -560, + 1600 ], "webhookId": "cba1c38c-37bc-4acb-bd61-48035604a5c4" }, @@ -284,13 +284,13 @@ "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) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[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 ? `${title} · GLM Hub
\"GomezLee

GLM HUB · DECISIÓN DE ACCESO

${title}

Vas a ${approved ? 'aprobar' : 'rechazar'} la solicitud de acceso a ${escapeHtml(appName)}. La decisión será definitiva y los otros enlaces quedarán sin efecto.

Cierra esta pestaña para cancelar sin registrar ninguna decisión.

`\n : `
\"GomezLee

Enlace no válido

El enlace está incompleto o no corresponde a una solicitud de GLM Hub.

`;\nreturn [{ json: { html } }];" }, - "id": "66f986d2-7c8b-4c25-9a5c-4310f5c37d3a", + "id": "1cafd88f-b081-45fe-ab11-9de66d2337e0", "name": "Construir página de confirmación", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 656, - 896 + -336, + 1600 ] }, { @@ -317,13 +317,13 @@ } } }, - "id": "abba0ac3-e1fc-4c2a-a6fc-ecba5e924cdf", + "id": "7827c03c-dae8-4b9b-9c31-7f2a3a1e7dfe", "name": "Mostrar confirmación", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ - 880, - 896 + -112, + 1600 ] }, { @@ -333,13 +333,13 @@ "responseMode": "responseNode", "options": {} }, - "id": "9baccfdb-9561-4e85-bf43-7993eb6bc65d", + "id": "1cc0149c-206d-479e-b432-d9060bebb79d", "name": "Webhook - Resolver solicitud", "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ - -544, - 1584 + -1536, + 2288 ], "webhookId": "54eeee1b-b24b-46ae-84dc-70c4103e3a3a" }, @@ -347,13 +347,13 @@ "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", + "id": "864d54fe-72f0-488a-bff6-0de0d9044308", "name": "Validar decisión", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - -320, - 1584 + -1312, + 2288 ] }, { @@ -381,26 +381,26 @@ }, "options": {} }, - "id": "dfacee52-d608-4086-b287-560ce870c980", + "id": "07f79dcf-3d55-4122-9713-f3a1009e4657", "name": "¿Decisión válida?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ - -96, - 1584 + -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) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));\nconst html = `
\"GomezLee

No se pudo procesar

${escapeHtml(message)}

`;\nreturn [{ json: { html } }];" }, - "id": "8bebdbfd-3ee5-4a87-a0d4-c0b3a9bd17d4", + "id": "da7d60fb-beb0-4a3e-94e7-31cd643b40c7", "name": "Preparar decisión inválida", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 128, - 1776 + -864, + 2480 ] }, { @@ -427,13 +427,13 @@ } } }, - "id": "bdaa54d2-e20d-4523-963b-1cc88f4d8669", + "id": "1154375e-9680-4582-8bdd-514b15d27726", "name": "Responder decisión inválida", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ - 368, - 1776 + -624, + 2480 ] }, { @@ -474,26 +474,26 @@ "timeout": 30000 } }, - "id": "df9c350b-10f7-494a-8c5e-e0354c1606db", + "id": "f453b62b-82ef-49e1-8b7a-e50a51a14fa3", "name": "Registrar decisión en Supabase", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 128, - 1504 + -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) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[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 = `${responseTitle} · GLM Hub
\"GomezLee

GLM HUB · SOLICITUD DE ACCESO

${responseTitle}

${responseMessage}

Ya puedes cerrar esta pestaña.

`;\nconst itHtml = ok ? `
\"GomezLee
GLM HUB · DECISIÓN REGISTRADA

Solicitud ${statusLabel.toLowerCase()}.

IT Support debe continuar según la decisión indicada.

${statusLabel}
Persona solicitante${escapeHtml(requesterName)}
Correo solicitante${escapeHtml(requesterEmail)}
Aplicación${escapeHtml(appName)}
Decisión${statusLabel}
Decidido por${escapeHtml(decidedByName)}
Correo de quien decidió${escapeHtml(decidedByEmail)}
GomezLee Marketing · GLM Hub · Uso interno
` : '';\nreturn [{ json: { notifyIt: ok, responseHtml, itHtml, subject: `Acceso ${approved ? 'aprobado' : 'rechazado'} | ${appName} | ${requesterName}` } }];" }, - "id": "72ee2fcd-bf95-4542-aad1-90ca4d83a2e7", + "id": "6913ff01-1482-4413-964f-eff10fa4a64e", "name": "Preparar resultado de decisión", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 1344, - 1520 + 352, + 2224 ] }, { @@ -521,13 +521,13 @@ }, "options": {} }, - "id": "9f4e6707-3cb1-4c0e-833c-05be87602e07", + "id": "343ab147-11df-4030-96b9-b750e6151100", "name": "¿Notificar a IT Support?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ - 1584, - 1520 + 592, + 2224 ] }, { @@ -540,13 +540,13 @@ "senderName": "GLM Hub" } }, - "id": "56d73834-fc03-43c7-9844-dd4e691439b2", + "id": "c1be686a-a5a6-4285-abbd-bf9ed90247d0", "name": "Enviar decisión a IT Support", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [ - 1824, - 1424 + 832, + 2128 ], "webhookId": "234c8e3b-b904-41b8-8a74-9ca835f759df", "credentials": { @@ -580,26 +580,26 @@ } } }, - "id": "e1d4a5ba-34fe-479a-a670-e07ebd87d0cf", + "id": "f3f941b3-b902-4ed6-abd4-ec3e00baee36", "name": "Mostrar resultado final", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ - 2064, - 1520 + 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": "e2cc29db-5db2-46ea-a388-054c320ecfc2", + "id": "5e121030-2188-415d-8013-a54d58fedc91", "name": "Confirmar registro de solicitud", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 512, - 112 + -480, + 816 ] }, { @@ -627,13 +627,13 @@ }, "options": {} }, - "id": "7f6803be-bc3a-45e1-b1a4-c04c33ca25b9", + "id": "f722416a-53e0-403d-a301-8bfe4d43de60", "name": "¿Solicitud registrada en Supabase?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ - 736, - 112 + -256, + 816 ] }, { @@ -674,13 +674,13 @@ "timeout": 30000 } }, - "id": "d684c268-bbd8-4a3b-8aab-b21510423c95", + "id": "dfe96316-ab43-49e2-b9fb-384d34c5389c", "name": "Emitir enlaces de aprobación", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 1200, - 64 + 208, + 768 ] }, { @@ -693,10 +693,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - -1024, - -16 + -2016, + 688 ], - "id": "55489511-4594-4d60-8b68-87f0210e4b9e", + "id": "97451dbc-cd46-40e8-911b-7c4cf3e648f4", "name": "Sticky Note" }, { @@ -709,15 +709,15 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - 192, - -64 + -800, + 640 ], - "id": "729f2bb0-c1f2-424f-ae39-bf69ea94317c", + "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- Construye enlaces únicos para la aprobación y el rechazo.\n- Genera el contenido personalizado de los correos.\n- Envía la solicitud a los aprobadores mediante Gmail.\n- Confirma al GLM Hub que la solicitud fue enviada correctamente.\n- Devuelve un error controlado si no fue posible completar el registro.", + "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" @@ -725,10 +725,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - 1104, - -224 + 112, + 480 ], - "id": "32a9a9ad-bd7c-4d67-93db-d6537fd3a978", + "id": "bab6dbac-ae4b-46ee-b31e-e894aa1fc20a", "name": "Sticky Note2" }, { @@ -741,10 +741,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - 192, - 720 + -800, + 1424 ], - "id": "305bb9cd-16f5-4f87-a3b4-f6009a683085", + "id": "9115ee9e-a2ce-419f-a783-62cfe268c8fd", "name": "Sticky Note3" }, { @@ -757,10 +757,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - -624, - 1328 + -1616, + 2032 ], - "id": "0289d494-ec4a-4f09-89ed-02006e0fcc4e", + "id": "68fac86e-bbee-4648-b2ed-bfe3b9e9829c", "name": "Sticky Note4" }, { @@ -773,10 +773,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - 1184, - 1264 + 192, + 1968 ], - "id": "7881e095-8b12-4b96-83fd-a174d8dd0703", + "id": "024b61e1-bbc8-4838-9afe-7ee36445d0db", "name": "Sticky Note5" } ], @@ -1046,9 +1046,9 @@ "meta": { "templateCredsSetupCompleted": true }, - "versionId": "dc3ed690-5728-414e-a322-137e35e8f9b6", - "activeVersionId": "dc3ed690-5728-414e-a322-137e35e8f9b6", - "versionCounter": 137, + "versionId": "f0bf0a23-7940-4d51-b12c-00ae0fd7cc50", + "activeVersionId": "f0bf0a23-7940-4d51-b12c-00ae0fd7cc50", + "versionCounter": 149, "triggerCount": 3, "shared": [ { @@ -1071,9 +1071,9 @@ ], "tags": [], "activeVersion": { - "updatedAt": "2026-08-06T17:47:21.000Z", - "createdAt": "2026-08-06T17:47:19.081Z", - "versionId": "dc3ed690-5728-414e-a322-137e35e8f9b6", + "updatedAt": "2026-08-15T13:32:14.000Z", + "createdAt": "2026-08-15T13:31:33.472Z", + "versionId": "f0bf0a23-7940-4d51-b12c-00ae0fd7cc50", "workflowId": "RaNkDfE2YCrZgCbx", "nodes": [ { @@ -1085,13 +1085,13 @@ "allowedOrigins": "*" } }, - "id": "80c8fbc9-c126-447e-a2b3-b2c9ac6e9468", + "id": "6fee8ce5-2837-4683-9b9c-fd1022a87f5c", "name": "Webhook - Solicitar acceso", "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ - -928, - 192 + -1920, + 896 ], "webhookId": "ad3e98b9-baf2-400f-90e7-dcd4c5c20457" }, @@ -1099,13 +1099,13 @@ "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", + "id": "05811eaf-72a8-448c-8c01-16b9d9d7a592", "name": "Validar solicitud de acceso", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - -704, - 192 + -1696, + 896 ] }, { @@ -1133,13 +1133,13 @@ }, "options": {} }, - "id": "3c1cd649-6e3a-4751-ba1f-e775e8040f1d", + "id": "fddbe870-eea1-4b94-9e64-c4aec9c4dad7", "name": "¿Solicitud de acceso válida?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ - -480, - 192 + -1472, + 896 ] }, { @@ -1158,13 +1158,13 @@ } } }, - "id": "ff32cfc2-c5ed-4e10-8804-2209cd17ef69", + "id": "397ca441-991c-4f5d-9ce8-1df22a9fccc8", "name": "Responder solicitud inválida", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ - -160, - 352 + -1152, + 1056 ] }, { @@ -1201,26 +1201,26 @@ "timeout": 30000 } }, - "id": "260ee2e0-d0d7-4395-a186-b6cec2a766db", + "id": "1315636b-e124-4a59-9308-61f6855c482a", "name": "Crear solicitud en Supabase", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 304, - 112 + -688, + 816 ] }, { "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) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[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 = `
\"GomezLee
GLM HUB · SOLICITUD DE ACCESO

Una persona necesita acceso.

Hola ${escapeHtml(approver.name)}, revisa la siguiente solicitud.

Solicitante${escapeHtml(requesterName)}
Correo${escapeHtml(requesterEmail)}
Aplicación${escapeHtml(appName)}
Área${escapeHtml(appCategory)}

Por seguridad, cada botón abre una pantalla de confirmación. La primera decisión confirmada será definitiva para los tres destinatarios.

Aceptar solicitudRechazar solicitud
Solicitud ${escapeHtml(requestId)} · GomezLee Marketing · Uso interno
`;\n return { json: { created: true, requestId, sendTo: String(approver.email ?? ''), subject: `Solicitud de acceso | ${appName} | ${requesterName}`, html } };\n});" + "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 '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n \"'\": '''\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\n\n\n\n\n \n \n\n \n\n \n \n \n \n \n\n \n \n\n \n GLM HUB · SOLICITUD DE ACCESO\n \n\n \n Una persona necesita acceso.\n \n\n \n Hola ${escapeHtml(approver.name)}, revisa la siguiente solicitud.\n

\n\n \n \n\n \n \n\n \n\n \n \n Solicitante\n \n\n \n ${escapeHtml(requesterName)}\n \n \n\n \n \n Correo\n \n\n \n ${escapeHtml(requesterEmail)}\n \n \n\n \n \n Aplicación\n \n\n \n ${escapeHtml(appName)}\n \n \n\n \n \n Área\n \n\n \n ${escapeHtml(appCategory)}\n \n \n\n \n\n \n \n\n \n \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

\n\n \n \n\n \n \n Aceptar solicitud\n \n \n\n \n \n Rechazar solicitud\n \n \n\n \n \n\n \n \n\n \n \n GomezLee Marketing · Uso interno\n \n \n\n \n\n \n \n\n\n\n`;\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": "c4bd59ac-3233-4d09-81af-17f08b9325f5", + "id": "b38bcf45-4f58-4997-87e2-247c6478bdc2", "name": "Preparar correos de aprobación", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 1440, - 64 + 448, + 768 ] }, { @@ -1248,13 +1248,13 @@ }, "options": {} }, - "id": "eae9bf96-c598-42e0-b4df-c58319e3c354", + "id": "d1c724f7-4048-4545-b055-55d355fbe4bc", "name": "¿Solicitud registrada?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ - 1680, - 64 + 688, + 768 ] }, { @@ -1267,13 +1267,13 @@ "senderName": "GLM Hub" } }, - "id": "f3f7f82d-d96e-499f-a2d6-9e26f5496bb1", + "id": "b1a5605e-0ad9-4cae-8290-dec2427f645f", "name": "Enviar solicitud a aprobadores", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [ - 1920, - -32 + 928, + 672 ], "webhookId": "7dc65e7b-4fb6-4a2f-b685-34171dd9ce1a", "credentials": { @@ -1286,7 +1286,7 @@ { "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 } }}", + "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": { @@ -1299,13 +1299,13 @@ } } }, - "id": "af925eb2-9fb8-4261-8652-4e162969e512", + "id": "8280f9b8-18ee-456c-8e71-2c923f8e60ec", "name": "Responder solicitud enviada", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ - 2160, - -32 + 1168, + 672 ] }, { @@ -1324,13 +1324,13 @@ } } }, - "id": "0a104b37-5336-44ce-953b-979ce5faaa87", + "id": "59696c39-a80c-4548-80a8-162d63ea1346", "name": "Responder error al registrar", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ - 1920, - 176 + 928, + 880 ] }, { @@ -1339,13 +1339,13 @@ "responseMode": "responseNode", "options": {} }, - "id": "569dfd6e-b94f-4db8-8c88-4a890cff1ea3", + "id": "e0152ea1-39a8-49e1-add4-ec5f83b7b288", "name": "Webhook - Confirmar decisión", "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ - 432, - 896 + -560, + 1600 ], "webhookId": "cba1c38c-37bc-4acb-bd61-48035604a5c4" }, @@ -1353,13 +1353,13 @@ "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) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[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 ? `${title} · GLM Hub
\"GomezLee

GLM HUB · DECISIÓN DE ACCESO

${title}

Vas a ${approved ? 'aprobar' : 'rechazar'} la solicitud de acceso a ${escapeHtml(appName)}. La decisión será definitiva y los otros enlaces quedarán sin efecto.

Cierra esta pestaña para cancelar sin registrar ninguna decisión.

`\n : `
\"GomezLee

Enlace no válido

El enlace está incompleto o no corresponde a una solicitud de GLM Hub.

`;\nreturn [{ json: { html } }];" }, - "id": "66f986d2-7c8b-4c25-9a5c-4310f5c37d3a", + "id": "1cafd88f-b081-45fe-ab11-9de66d2337e0", "name": "Construir página de confirmación", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 656, - 896 + -336, + 1600 ] }, { @@ -1386,13 +1386,13 @@ } } }, - "id": "abba0ac3-e1fc-4c2a-a6fc-ecba5e924cdf", + "id": "7827c03c-dae8-4b9b-9c31-7f2a3a1e7dfe", "name": "Mostrar confirmación", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ - 880, - 896 + -112, + 1600 ] }, { @@ -1402,13 +1402,13 @@ "responseMode": "responseNode", "options": {} }, - "id": "9baccfdb-9561-4e85-bf43-7993eb6bc65d", + "id": "1cc0149c-206d-479e-b432-d9060bebb79d", "name": "Webhook - Resolver solicitud", "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ - -544, - 1584 + -1536, + 2288 ], "webhookId": "54eeee1b-b24b-46ae-84dc-70c4103e3a3a" }, @@ -1416,13 +1416,13 @@ "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", + "id": "864d54fe-72f0-488a-bff6-0de0d9044308", "name": "Validar decisión", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - -320, - 1584 + -1312, + 2288 ] }, { @@ -1450,26 +1450,26 @@ }, "options": {} }, - "id": "dfacee52-d608-4086-b287-560ce870c980", + "id": "07f79dcf-3d55-4122-9713-f3a1009e4657", "name": "¿Decisión válida?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ - -96, - 1584 + -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) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));\nconst html = `
\"GomezLee

No se pudo procesar

${escapeHtml(message)}

`;\nreturn [{ json: { html } }];" }, - "id": "8bebdbfd-3ee5-4a87-a0d4-c0b3a9bd17d4", + "id": "da7d60fb-beb0-4a3e-94e7-31cd643b40c7", "name": "Preparar decisión inválida", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 128, - 1776 + -864, + 2480 ] }, { @@ -1496,13 +1496,13 @@ } } }, - "id": "bdaa54d2-e20d-4523-963b-1cc88f4d8669", + "id": "1154375e-9680-4582-8bdd-514b15d27726", "name": "Responder decisión inválida", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ - 368, - 1776 + -624, + 2480 ] }, { @@ -1543,26 +1543,26 @@ "timeout": 30000 } }, - "id": "df9c350b-10f7-494a-8c5e-e0354c1606db", + "id": "f453b62b-82ef-49e1-8b7a-e50a51a14fa3", "name": "Registrar decisión en Supabase", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 128, - 1504 + -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) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[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 = `${responseTitle} · GLM Hub
\"GomezLee

GLM HUB · SOLICITUD DE ACCESO

${responseTitle}

${responseMessage}

Ya puedes cerrar esta pestaña.

`;\nconst itHtml = ok ? `
\"GomezLee
GLM HUB · DECISIÓN REGISTRADA

Solicitud ${statusLabel.toLowerCase()}.

IT Support debe continuar según la decisión indicada.

${statusLabel}
Persona solicitante${escapeHtml(requesterName)}
Correo solicitante${escapeHtml(requesterEmail)}
Aplicación${escapeHtml(appName)}
Decisión${statusLabel}
Decidido por${escapeHtml(decidedByName)}
Correo de quien decidió${escapeHtml(decidedByEmail)}
GomezLee Marketing · GLM Hub · Uso interno
` : '';\nreturn [{ json: { notifyIt: ok, responseHtml, itHtml, subject: `Acceso ${approved ? 'aprobado' : 'rechazado'} | ${appName} | ${requesterName}` } }];" }, - "id": "72ee2fcd-bf95-4542-aad1-90ca4d83a2e7", + "id": "6913ff01-1482-4413-964f-eff10fa4a64e", "name": "Preparar resultado de decisión", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 1344, - 1520 + 352, + 2224 ] }, { @@ -1590,13 +1590,13 @@ }, "options": {} }, - "id": "9f4e6707-3cb1-4c0e-833c-05be87602e07", + "id": "343ab147-11df-4030-96b9-b750e6151100", "name": "¿Notificar a IT Support?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ - 1584, - 1520 + 592, + 2224 ] }, { @@ -1609,13 +1609,13 @@ "senderName": "GLM Hub" } }, - "id": "56d73834-fc03-43c7-9844-dd4e691439b2", + "id": "c1be686a-a5a6-4285-abbd-bf9ed90247d0", "name": "Enviar decisión a IT Support", "type": "n8n-nodes-base.gmail", "typeVersion": 2.1, "position": [ - 1824, - 1424 + 832, + 2128 ], "webhookId": "234c8e3b-b904-41b8-8a74-9ca835f759df", "credentials": { @@ -1649,26 +1649,26 @@ } } }, - "id": "e1d4a5ba-34fe-479a-a670-e07ebd87d0cf", + "id": "f3f941b3-b902-4ed6-abd4-ec3e00baee36", "name": "Mostrar resultado final", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1.4, "position": [ - 2064, - 1520 + 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": "e2cc29db-5db2-46ea-a388-054c320ecfc2", + "id": "5e121030-2188-415d-8013-a54d58fedc91", "name": "Confirmar registro de solicitud", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ - 512, - 112 + -480, + 816 ] }, { @@ -1696,13 +1696,13 @@ }, "options": {} }, - "id": "7f6803be-bc3a-45e1-b1a4-c04c33ca25b9", + "id": "f722416a-53e0-403d-a301-8bfe4d43de60", "name": "¿Solicitud registrada en Supabase?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ - 736, - 112 + -256, + 816 ] }, { @@ -1743,13 +1743,13 @@ "timeout": 30000 } }, - "id": "d684c268-bbd8-4a3b-8aab-b21510423c95", + "id": "dfe96316-ab43-49e2-b9fb-384d34c5389c", "name": "Emitir enlaces de aprobación", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ - 1200, - 64 + 208, + 768 ] }, { @@ -1762,10 +1762,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - -1024, - -16 + -2016, + 688 ], - "id": "55489511-4594-4d60-8b68-87f0210e4b9e", + "id": "97451dbc-cd46-40e8-911b-7c4cf3e648f4", "name": "Sticky Note" }, { @@ -1778,15 +1778,15 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - 192, - -64 + -800, + 640 ], - "id": "729f2bb0-c1f2-424f-ae39-bf69ea94317c", + "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- Construye enlaces únicos para la aprobación y el rechazo.\n- Genera el contenido personalizado de los correos.\n- Envía la solicitud a los aprobadores mediante Gmail.\n- Confirma al GLM Hub que la solicitud fue enviada correctamente.\n- Devuelve un error controlado si no fue posible completar el registro.", + "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" @@ -1794,10 +1794,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - 1104, - -224 + 112, + 480 ], - "id": "32a9a9ad-bd7c-4d67-93db-d6537fd3a978", + "id": "bab6dbac-ae4b-46ee-b31e-e894aa1fc20a", "name": "Sticky Note2" }, { @@ -1810,10 +1810,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - 192, - 720 + -800, + 1424 ], - "id": "305bb9cd-16f5-4f87-a3b4-f6009a683085", + "id": "9115ee9e-a2ce-419f-a783-62cfe268c8fd", "name": "Sticky Note3" }, { @@ -1826,10 +1826,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - -624, - 1328 + -1616, + 2032 ], - "id": "0289d494-ec4a-4f09-89ed-02006e0fcc4e", + "id": "68fac86e-bbee-4648-b2ed-bfe3b9e9829c", "name": "Sticky Note4" }, { @@ -1842,10 +1842,10 @@ "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - 1184, - 1264 + 192, + 1968 ], - "id": "7881e095-8b12-4b96-83fd-a174d8dd0703", + "id": "024b61e1-bbc8-4838-9afe-7ee36445d0db", "name": "Sticky Note5" } ], @@ -2107,15 +2107,15 @@ } }, "authors": "Isaac Aracena", - "name": "Version dc3ed690", + "name": "Version f0bf0a23", "description": "", - "autosaved": false, + "autosaved": true, "workflowPublishHistory": [ { - "createdAt": "2026-08-06T17:47:21.133Z", - "id": 4229, + "createdAt": "2026-08-15T13:32:14.699Z", + "id": 4768, "workflowId": "RaNkDfE2YCrZgCbx", - "versionId": "dc3ed690-5728-414e-a322-137e35e8f9b6", + "versionId": "f0bf0a23-7940-4d51-b12c-00ae0fd7cc50", "event": "activated", "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" }