From b41d78a0c025f22e807ef774efafeab4fa5db249 Mon Sep 17 00:00:00 2001 From: Isaac_Aracena Date: Sun, 2 Aug 2026 04:00:40 +0000 Subject: [PATCH] =?UTF-8?q?Create=20backup:=20GLM=20Hub=20-=20Solicitudes?= =?UTF-8?q?=20de=20acceso=20y=20aprobaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ub-solicitudes-de-acceso-y-aprobacion.json | 1932 +++++++++++++++++ 1 file changed, 1932 insertions(+) create mode 100644 glm-hub-solicitudes-de-acceso-y-aprobacion.json diff --git a/glm-hub-solicitudes-de-acceso-y-aprobacion.json b/glm-hub-solicitudes-de-acceso-y-aprobacion.json new file mode 100644 index 0000000..086605e --- /dev/null +++ b/glm-hub-solicitudes-de-acceso-y-aprobacion.json @@ -0,0 +1,1932 @@ +{ + "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) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[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});" + }, + "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) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[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", + "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) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));\nconst html = `
\"GomezLee

No se pudo procesar

${escapeHtml(message)}

`;\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) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[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", + "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 ", + "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) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[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});" + }, + "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) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[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", + "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) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));\nconst html = `
\"GomezLee

No se pudo procesar

${escapeHtml(message)}

`;\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) => ({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[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", + "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" + } + ] + } +} \ No newline at end of file