{ "updatedAt": "2026-08-06T17:47:19.077Z", "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": [ -928, 192 ], "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": [ -704, 192 ] }, { "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": [ -480, 192 ] }, { "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": [ -160, 352 ] }, { "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": [ 304, 112 ] }, { "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": [ 1440, 64 ] }, { "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": [ 1680, 64 ] }, { "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": [ 1920, -32 ], "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": [ 2160, -32 ] }, { "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": [ 1920, 176 ] }, { "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": [ 432, 896 ], "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": [ 656, 896 ] }, { "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": [ 880, 896 ] }, { "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": [ -544, 1584 ], "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": [ -320, 1584 ] }, { "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": [ -96, 1584 ] }, { "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": [ 128, 1776 ] }, { "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": [ 368, 1776 ] }, { "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": [ 128, 1504 ] }, { "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": [ 1344, 1520 ] }, { "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": [ 1584, 1520 ] }, { "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": [ 1824, 1424 ], "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": [ 2064, 1520 ] }, { "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": [ 512, 112 ] }, { "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": [ 736, 112 ] }, { "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": [ 1200, 64 ] }, { "parameters": { "content": "## 1. RECEPCIÓN Y VALIDACIÓN\n\nEste bloque recibe desde GLM Hub una nueva solicitud de acceso y valida que la información obligatoria sea correcta.\n\n- Verifica los datos del usuario y de la aplicación solicitada.\n- Rechaza inmediatamente solicitudes incompletas o inválidas.\n- Solo las solicitudes válidas continúan hacia el registro en Supabase.", "height": 560, "width": 1104, "color": 6 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -1024, -16 ], "id": "55489511-4594-4d60-8b68-87f0210e4b9e", "name": "Sticky Note" }, { "parameters": { "content": "## 2. REGISTRO DE LA SOLICITUD\n\nGuarda la solicitud de acceso en Supabase y confirma que el registro se haya creado correctamente.\n\n- Almacena la información de la solicitud y su estado inicial.\n- Recupera y normaliza los datos devueltos por Supabase.\n- Solo permite continuar cuando existe una confirmación válida del registro.", "height": 384, "width": 736, "color": 5 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 192, -64 ], "id": "729f2bb0-c1f2-424f-ae39-bf69ea94317c", "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.", "height": 560, "width": 1264, "color": "#357C27" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 1104, -224 ], "id": "32a9a9ad-bd7c-4d67-93db-d6537fd3a978", "name": "Sticky Note2" }, { "parameters": { "content": "## 4. CONFIRMACIÓN DE LA DECISIÓN\n\nRecibe al aprobador desde el enlace enviado por correo y muestra una página previa antes de registrar la decisión definitiva.\n\n- Lee los parámetros incluidos en el enlace de aprobación o rechazo.\n- Construye una página con los datos de la solicitud.\n- Solicita una confirmación final para evitar decisiones accidentales.", "height": 352, "width": 1062, "color": "#8F622D" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 192, 720 ], "id": "305bb9cd-16f5-4f87-a3b4-f6009a683085", "name": "Sticky Note3" }, { "parameters": { "content": "## 5. VALIDACIÓN Y REGISTRO DE LA DECISIÓN\n\nValida la decisión confirmada por el aprobador y actualiza la solicitud correspondiente en Supabase.\n\n- Comprueba que la solicitud, el enlace y la decisión sean válidos.\n- Registra en Supabase la aprobación o el rechazo.\n- Impide procesar decisiones incorrectas, incompletas o manipuladas.\n- Muestra una respuesta controlada cuando la decisión no puede ser aceptada.", "height": 624, "width": 1184, "color": "#52500F" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -624, 1328 ], "id": "0289d494-ec4a-4f09-89ed-02006e0fcc4e", "name": "Sticky Note4" }, { "parameters": { "content": "## 6. NOTIFICACIÓN Y RESULTADO FINAL\n\nPrepara el resultado de la decisión, determina si se necesita intervención de IT Support y finaliza el proceso.\n\n- Construye el mensaje final de aprobación o rechazo.\n- Evalúa si la decisión debe notificarse al equipo de IT.\n- Envía por Gmail la información necesaria cuando corresponde.\n- Muestra al aprobador el resultado definitivo de la operación.", "height": 432, "width": 1104, "color": "#276D4F" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 1184, 1264 ], "id": "7881e095-8b12-4b96-83fd-a174d8dd0703", "name": "Sticky Note5" } ], "connections": { "Webhook - Solicitar acceso": { "main": [ [ { "node": "Validar solicitud de acceso", "type": "main", "index": 0 } ] ] }, "Validar solicitud de acceso": { "main": [ [ { "node": "¿Solicitud de acceso válida?", "type": "main", "index": 0 } ] ] }, "¿Solicitud de acceso válida?": { "main": [ [ { "node": "Crear solicitud en Supabase", "type": "main", "index": 0 } ], [ { "node": "Responder solicitud inválida", "type": "main", "index": 0 } ] ] }, "Crear solicitud en Supabase": { "main": [ [ { "node": "Confirmar registro de solicitud", "type": "main", "index": 0 } ] ] }, "Preparar correos de aprobación": { "main": [ [ { "node": "¿Solicitud registrada?", "type": "main", "index": 0 } ] ] }, "¿Solicitud registrada?": { "main": [ [ { "node": "Enviar solicitud a aprobadores", "type": "main", "index": 0 } ], [ { "node": "Responder error al registrar", "type": "main", "index": 0 } ] ] }, "Enviar solicitud a aprobadores": { "main": [ [ { "node": "Responder solicitud enviada", "type": "main", "index": 0 } ] ] }, "Webhook - Confirmar decisión": { "main": [ [ { "node": "Construir página de confirmación", "type": "main", "index": 0 } ] ] }, "Construir página de confirmación": { "main": [ [ { "node": "Mostrar confirmación", "type": "main", "index": 0 } ] ] }, "Webhook - Resolver solicitud": { "main": [ [ { "node": "Validar decisión", "type": "main", "index": 0 } ] ] }, "Validar decisión": { "main": [ [ { "node": "¿Decisión válida?", "type": "main", "index": 0 } ] ] }, "¿Decisión válida?": { "main": [ [ { "node": "Registrar decisión en Supabase", "type": "main", "index": 0 } ], [ { "node": "Preparar decisión inválida", "type": "main", "index": 0 } ] ] }, "Preparar decisión inválida": { "main": [ [ { "node": "Responder decisión inválida", "type": "main", "index": 0 } ] ] }, "Registrar decisión en Supabase": { "main": [ [ { "node": "Preparar resultado de decisión", "type": "main", "index": 0 } ] ] }, "Preparar resultado de decisión": { "main": [ [ { "node": "¿Notificar a IT Support?", "type": "main", "index": 0 } ] ] }, "¿Notificar a IT Support?": { "main": [ [ { "node": "Enviar decisión a IT Support", "type": "main", "index": 0 } ], [ { "node": "Mostrar resultado final", "type": "main", "index": 0 } ] ] }, "Enviar decisión a IT Support": { "main": [ [ { "node": "Mostrar resultado final", "type": "main", "index": 0 } ] ] }, "Confirmar registro de solicitud": { "main": [ [ { "node": "¿Solicitud registrada en Supabase?", "type": "main", "index": 0 } ] ] }, "¿Solicitud registrada en Supabase?": { "main": [ [ { "node": "Emitir enlaces de aprobación", "type": "main", "index": 0 } ], [ { "node": "Responder error al registrar", "type": "main", "index": 0 } ] ] }, "Emitir enlaces de aprobación": { "main": [ [ { "node": "Preparar correos de aprobación", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1", "binaryMode": "separate", "availableInMCP": true }, "staticData": null, "meta": { "templateCredsSetupCompleted": true }, "versionId": "dc3ed690-5728-414e-a322-137e35e8f9b6", "activeVersionId": "dc3ed690-5728-414e-a322-137e35e8f9b6", "versionCounter": 137, "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-08-06T17:47:21.000Z", "createdAt": "2026-08-06T17:47:19.081Z", "versionId": "dc3ed690-5728-414e-a322-137e35e8f9b6", "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": [ -928, 192 ], "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": [ -704, 192 ] }, { "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": [ -480, 192 ] }, { "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": [ -160, 352 ] }, { "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": [ 304, 112 ] }, { "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": [ 1440, 64 ] }, { "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": [ 1680, 64 ] }, { "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": [ 1920, -32 ], "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": [ 2160, -32 ] }, { "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": [ 1920, 176 ] }, { "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": [ 432, 896 ], "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": [ 656, 896 ] }, { "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": [ 880, 896 ] }, { "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": [ -544, 1584 ], "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": [ -320, 1584 ] }, { "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": [ -96, 1584 ] }, { "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": [ 128, 1776 ] }, { "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": [ 368, 1776 ] }, { "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": [ 128, 1504 ] }, { "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": [ 1344, 1520 ] }, { "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": [ 1584, 1520 ] }, { "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": [ 1824, 1424 ], "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": [ 2064, 1520 ] }, { "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": [ 512, 112 ] }, { "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": [ 736, 112 ] }, { "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": [ 1200, 64 ] }, { "parameters": { "content": "## 1. RECEPCIÓN Y VALIDACIÓN\n\nEste bloque recibe desde GLM Hub una nueva solicitud de acceso y valida que la información obligatoria sea correcta.\n\n- Verifica los datos del usuario y de la aplicación solicitada.\n- Rechaza inmediatamente solicitudes incompletas o inválidas.\n- Solo las solicitudes válidas continúan hacia el registro en Supabase.", "height": 560, "width": 1104, "color": 6 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -1024, -16 ], "id": "55489511-4594-4d60-8b68-87f0210e4b9e", "name": "Sticky Note" }, { "parameters": { "content": "## 2. REGISTRO DE LA SOLICITUD\n\nGuarda la solicitud de acceso en Supabase y confirma que el registro se haya creado correctamente.\n\n- Almacena la información de la solicitud y su estado inicial.\n- Recupera y normaliza los datos devueltos por Supabase.\n- Solo permite continuar cuando existe una confirmación válida del registro.", "height": 384, "width": 736, "color": 5 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 192, -64 ], "id": "729f2bb0-c1f2-424f-ae39-bf69ea94317c", "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.", "height": 560, "width": 1264, "color": "#357C27" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 1104, -224 ], "id": "32a9a9ad-bd7c-4d67-93db-d6537fd3a978", "name": "Sticky Note2" }, { "parameters": { "content": "## 4. CONFIRMACIÓN DE LA DECISIÓN\n\nRecibe al aprobador desde el enlace enviado por correo y muestra una página previa antes de registrar la decisión definitiva.\n\n- Lee los parámetros incluidos en el enlace de aprobación o rechazo.\n- Construye una página con los datos de la solicitud.\n- Solicita una confirmación final para evitar decisiones accidentales.", "height": 352, "width": 1062, "color": "#8F622D" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 192, 720 ], "id": "305bb9cd-16f5-4f87-a3b4-f6009a683085", "name": "Sticky Note3" }, { "parameters": { "content": "## 5. VALIDACIÓN Y REGISTRO DE LA DECISIÓN\n\nValida la decisión confirmada por el aprobador y actualiza la solicitud correspondiente en Supabase.\n\n- Comprueba que la solicitud, el enlace y la decisión sean válidos.\n- Registra en Supabase la aprobación o el rechazo.\n- Impide procesar decisiones incorrectas, incompletas o manipuladas.\n- Muestra una respuesta controlada cuando la decisión no puede ser aceptada.", "height": 624, "width": 1184, "color": "#52500F" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -624, 1328 ], "id": "0289d494-ec4a-4f09-89ed-02006e0fcc4e", "name": "Sticky Note4" }, { "parameters": { "content": "## 6. NOTIFICACIÓN Y RESULTADO FINAL\n\nPrepara el resultado de la decisión, determina si se necesita intervención de IT Support y finaliza el proceso.\n\n- Construye el mensaje final de aprobación o rechazo.\n- Evalúa si la decisión debe notificarse al equipo de IT.\n- Envía por Gmail la información necesaria cuando corresponde.\n- Muestra al aprobador el resultado definitivo de la operación.", "height": 432, "width": 1104, "color": "#276D4F" }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 1184, 1264 ], "id": "7881e095-8b12-4b96-83fd-a174d8dd0703", "name": "Sticky Note5" } ], "connections": { "Webhook - Solicitar acceso": { "main": [ [ { "node": "Validar solicitud de acceso", "type": "main", "index": 0 } ] ] }, "Validar solicitud de acceso": { "main": [ [ { "node": "¿Solicitud de acceso válida?", "type": "main", "index": 0 } ] ] }, "¿Solicitud de acceso válida?": { "main": [ [ { "node": "Crear solicitud en Supabase", "type": "main", "index": 0 } ], [ { "node": "Responder solicitud inválida", "type": "main", "index": 0 } ] ] }, "Crear solicitud en Supabase": { "main": [ [ { "node": "Confirmar registro de solicitud", "type": "main", "index": 0 } ] ] }, "Preparar correos de aprobación": { "main": [ [ { "node": "¿Solicitud registrada?", "type": "main", "index": 0 } ] ] }, "¿Solicitud registrada?": { "main": [ [ { "node": "Enviar solicitud a aprobadores", "type": "main", "index": 0 } ], [ { "node": "Responder error al registrar", "type": "main", "index": 0 } ] ] }, "Enviar solicitud a aprobadores": { "main": [ [ { "node": "Responder solicitud enviada", "type": "main", "index": 0 } ] ] }, "Webhook - Confirmar decisión": { "main": [ [ { "node": "Construir página de confirmación", "type": "main", "index": 0 } ] ] }, "Construir página de confirmación": { "main": [ [ { "node": "Mostrar confirmación", "type": "main", "index": 0 } ] ] }, "Webhook - Resolver solicitud": { "main": [ [ { "node": "Validar decisión", "type": "main", "index": 0 } ] ] }, "Validar decisión": { "main": [ [ { "node": "¿Decisión válida?", "type": "main", "index": 0 } ] ] }, "¿Decisión válida?": { "main": [ [ { "node": "Registrar decisión en Supabase", "type": "main", "index": 0 } ], [ { "node": "Preparar decisión inválida", "type": "main", "index": 0 } ] ] }, "Preparar decisión inválida": { "main": [ [ { "node": "Responder decisión inválida", "type": "main", "index": 0 } ] ] }, "Registrar decisión en Supabase": { "main": [ [ { "node": "Preparar resultado de decisión", "type": "main", "index": 0 } ] ] }, "Preparar resultado de decisión": { "main": [ [ { "node": "¿Notificar a IT Support?", "type": "main", "index": 0 } ] ] }, "¿Notificar a IT Support?": { "main": [ [ { "node": "Enviar decisión a IT Support", "type": "main", "index": 0 } ], [ { "node": "Mostrar resultado final", "type": "main", "index": 0 } ] ] }, "Enviar decisión a IT Support": { "main": [ [ { "node": "Mostrar resultado final", "type": "main", "index": 0 } ] ] }, "Confirmar registro de solicitud": { "main": [ [ { "node": "¿Solicitud registrada en Supabase?", "type": "main", "index": 0 } ] ] }, "¿Solicitud registrada en Supabase?": { "main": [ [ { "node": "Emitir enlaces de aprobación", "type": "main", "index": 0 } ], [ { "node": "Responder error al registrar", "type": "main", "index": 0 } ] ] }, "Emitir enlaces de aprobación": { "main": [ [ { "node": "Preparar correos de aprobación", "type": "main", "index": 0 } ] ] } }, "authors": "Isaac Aracena", "name": "Version dc3ed690", "description": "", "autosaved": false, "workflowPublishHistory": [ { "createdAt": "2026-08-06T17:47:21.133Z", "id": 4229, "workflowId": "RaNkDfE2YCrZgCbx", "versionId": "dc3ed690-5728-414e-a322-137e35e8f9b6", "event": "activated", "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" } ] } }