Files
lucozade-audit-dashboard/lucozade-audit-dashboard.json
T

2343 lines
88 KiB
JSON

{
"updatedAt": "2026-08-06T18:19:46.432Z",
"createdAt": "2026-08-05T19:32:17.425Z",
"id": "WhKNJq6Dsu9Gmii7",
"name": "Lucozade Audit Dashboard",
"description": null,
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "lucozade-auth-v6",
"responseMode": "responseNode",
"options": {
"allowedOrigins": "https://digitalcompass.agency,http://localhost,http://127.0.0.1,http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173,http://127.0.0.1:5173,http://localhost:4173,http://127.0.0.1:4173"
}
},
"id": "3c56f534-1566-44ea-aca4-057e52576aa5",
"name": "Lucozade Auth Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
2704,
3808
],
"webhookId": "07ae0590-c2f5-43e5-94ea-903d7b74f3ec"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "7ec43804-056e-4289-afc6-f4c92a178118",
"name": "supabaseUrl",
"value": "https://dbit.digitalcompass.agency",
"type": "string"
},
{
"id": "0e5dd6d0-f0a4-4fe1-a515-51d74eb6446e",
"name": "serviceRoleKey",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q",
"type": "string"
},
{
"id": "813d005b-5e39-42e1-b79a-18ef997d2632",
"name": "productionAppUrl",
"value": "https://digitalcompass.agency/lucozade/",
"type": "string"
},
{
"id": "e02a637e-4042-4f26-8810-18ece84142a7",
"name": "appName",
"value": "Lucozade Store Audit",
"type": "string"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "3c7977cc-17b4-4358-a264-837067faabcb",
"name": "CONFIG · Supabase",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
2944,
3808
]
},
{
"parameters": {
"jsCode": "const source = $input.first().json || {};\nconst body = source.body || {};\nconst headers = Object.fromEntries(\n Object.entries(source.headers || {}).map(([key, value]) => [String(key).toLowerCase(), String(value ?? '')])\n);\n\nconst action = String(body.action || '').trim().toLowerCase();\nconst email = String(body.email || '').trim().toLowerCase();\nconst fullName = String(body.fullName || '').trim();\nconst password = String(body.password || '');\nconst honeypot = String(body.website || '').trim();\nconst requestedRedirect = String(body.redirectUrl || '').trim();\nconst ip = String(headers['x-forwarded-for'] || headers['x-real-ip'] || 'unknown').split(',')[0].trim();\n\nconst config = $('CONFIG · Supabase').first().json || {};\nconst supabaseUrl = String(config.supabaseUrl || '').replace(/\\/$/, '');\nconst productionAppUrl = String(config.productionAppUrl || '').trim();\nconst appName = String(config.appName || 'Lucozade Store Audit').trim();\nconst serviceRoleKey = String(config.serviceRoleKey || '').trim();\n\nconst allowedRedirects = [\n 'https://digitalcompass.agency/lucozade/',\n 'http://localhost:3000/',\n 'http://127.0.0.1:3000/',\n 'http://localhost:5173/',\n 'http://127.0.0.1:5173/',\n 'http://localhost:4173/lucozade/',\n 'http://127.0.0.1:4173/lucozade/'\n];\nconst redirectUrl = allowedRedirects.includes(requestedRedirect) ? requestedRedirect : productionAppUrl;\n\nlet valid = true;\nlet statusCode = 400;\nlet message = '';\n\nif (!supabaseUrl || !productionAppUrl || !serviceRoleKey || serviceRoleKey.includes('PASTE_')) {\n valid = false;\n statusCode = 500;\n message = 'The authentication service is not configured.';\n} else if (honeypot) {\n valid = false;\n message = 'Invalid request.';\n} else if (!['register', 'recover'].includes(action)) {\n valid = false;\n message = 'Invalid authentication action.';\n} else if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email) || email.length > 254) {\n valid = false;\n message = 'Enter a valid email address.';\n} else if (action === 'register' && (fullName.length < 2 || fullName.length > 120)) {\n valid = false;\n message = 'Enter your name.';\n} else if (action === 'register' && (password.length < 8 || password.length > 72)) {\n valid = false;\n message = 'Use a password between 8 and 72 characters.';\n}\n\nif (valid) {\n const staticData = $getWorkflowStaticData('global');\n staticData.lucozadeAuthRateLimitV6 = staticData.lucozadeAuthRateLimitV6 || {};\n const now = Date.now();\n const windowMs = 15 * 60 * 1000;\n const key = `${ip}:${action}:${email}`;\n const attempts = (staticData.lucozadeAuthRateLimitV6[key] || []).filter((time) => now - Number(time) < windowMs);\n\n if (attempts.length >= 10) {\n valid = false;\n statusCode = 429;\n message = 'Too many attempts. Please wait 15 minutes and try again.';\n } else {\n attempts.push(now);\n staticData.lucozadeAuthRateLimitV6[key] = attempts;\n }\n\n for (const [storedKey, times] of Object.entries(staticData.lucozadeAuthRateLimitV6)) {\n const recent = Array.isArray(times) ? times.filter((time) => now - Number(time) < windowMs) : [];\n if (recent.length) staticData.lucozadeAuthRateLimitV6[storedKey] = recent;\n else delete staticData.lucozadeAuthRateLimitV6[storedKey];\n }\n}\n\nreturn [{ json: {\n valid,\n statusCode,\n response: { ok: valid, message: valid ? 'Request accepted.' : message },\n action,\n email,\n fullName,\n password,\n redirectUrl,\n appName,\n supabaseUrl,\n ip\n}}];"
},
"id": "df0717c9-4552-462a-9112-5cb87f033aa9",
"name": "Validate Request",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3184,
3808
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "a5e66cca-1fa1-420c-9f6a-2852ce2ebcec",
"leftValue": "={{ $json.valid }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "90488719-d6c7-4b08-a586-c9170fd37c37",
"name": "Request Valid?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
3424,
3808
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6216684f-1892-4ad4-b321-3ff29f872514",
"leftValue": "={{ $json.action }}",
"rightValue": "register",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "ea555e17-eba1-43ab-832a-c7f7f8bb8c8f",
"name": "Register Request?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
3664,
3696
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $('CONFIG · Supabase').first().json.supabaseUrl.replace(/\\/$/, '') + '/auth/v1/admin/users' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { email: $json.email, password: $json.password, email_confirm: true, user_metadata: { full_name: $json.fullName, app_id: 'lucozade-audit' } } }}",
"options": {
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "4ea8c30e-e72f-4dfe-8df9-1730321276ec",
"name": "Create Supabase User",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
4448,
3408
]
},
{
"parameters": {
"jsCode": "const request = $('Validate Request').first().json;\nconst response = $input.first().json || {};\nconst hasEnvelope = Object.prototype.hasOwnProperty.call(response, 'statusCode') || Object.prototype.hasOwnProperty.call(response, 'body');\nconst statusCode = Number(response.statusCode || response.status || 0);\nconst body = response.body ?? response.data ?? response;\nconst user = body?.user || body || {};\nconst userId = String(user?.id || '').trim();\nconst apiOk = hasEnvelope ? statusCode >= 200 && statusCode < 300 : Boolean(userId);\nconst errorText = [body?.message, body?.msg, body?.error, body?.error_description, body?.code, body?.error_code]\n .filter(Boolean).join(' ');\nconst duplicate = /already.*registered|already exists|user.*exists|email_exists|user_already_exists|duplicate/i.test(errorText);\n\nreturn [{ json: {\n ...request,\n registrationCreated: apiOk && Boolean(userId),\n userId,\n statusCode: apiOk && userId ? 201 : (duplicate ? 409 : ([401,403].includes(statusCode) ? 500 : 502)),\n response: apiOk && userId\n ? { ok: true, message: 'Account created successfully.' }\n : duplicate\n ? { ok: false, message: 'An account already exists for this email. Use Sign in or Forgot password.' }\n : { ok: false, message: [401,403].includes(statusCode)\n ? 'The authentication service is not configured correctly.'\n : 'The account could not be created. Please try again.' }\n}}];"
},
"id": "0d76bf29-ed1b-4cdc-831e-102b61ee96a7",
"name": "Prepare Registration",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4688,
3408
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "ed820bef-c9f1-4d58-a327-ef60975043d8",
"leftValue": "={{ $json.registrationCreated }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "956fd0d2-914c-4a93-b729-2211cb8c031c",
"name": "Registration Created?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
4928,
3408
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $('CONFIG · Supabase').first().json.supabaseUrl.replace(/\\/$/, '') + '/rest/v1/lucozade_access?on_conflict=user_id' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "resolution=merge-duplicates,return=representation"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { user_id: $json.userId, email: $json.email, full_name: $json.fullName, is_active: true } }}",
"options": {
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "82a01c0a-ca12-41ab-a29e-6614b6ff7f7c",
"name": "Grant Lucozade Access",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
5168,
3312
]
},
{
"parameters": {
"jsCode": "const request = $('Prepare Registration').first().json;\nconst response = $input.first().json || {};\nconst hasEnvelope = Object.prototype.hasOwnProperty.call(response, 'statusCode') || Object.prototype.hasOwnProperty.call(response, 'body');\nconst statusCode = Number(response.statusCode || response.status || 0);\nconst body = response.body ?? response.data ?? response;\nconst accessGranted = hasEnvelope ? statusCode >= 200 && statusCode < 300 : Array.isArray(body);\nreturn [{ json: {\n ...request,\n accessGranted,\n statusCode: accessGranted ? 201 : 502,\n response: accessGranted\n ? { ok: true, message: 'Account created successfully.' }\n : { ok: false, message: 'The account could not be completed. Please try again.' }\n}}];"
},
"id": "be8d79f3-ab97-4aba-9642-7eb3910d5e02",
"name": "Prepare Access Grant",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
5408,
3312
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "e4dbcaf7-fc7a-47d9-a275-f8d81c669710",
"leftValue": "={{ $json.accessGranted }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "9d2ff8a2-bd1d-48ec-ab32-459b989d8d39",
"name": "Access Granted?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
5648,
3312
]
},
{
"parameters": {
"jsCode": "const request = $('Prepare Access Grant').first().json;\nconst escapeHtml = (value) => String(value ?? '')\n .replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')\n .replaceAll('\"', '&quot;').replaceAll(\"'\", '&#039;');\nconst safeName = escapeHtml(request.fullName || 'there');\nconst safeApp = escapeHtml(request.appName || 'Lucozade Store Audit');\nconst safeUrl = escapeHtml(request.redirectUrl);\nconst subject = 'Your Lucozade Store Audit account is ready';\nconst html = `<!doctype html><html lang=\"en\"><body style=\"margin:0;background:#f4f8fb;padding:28px 12px;\">\n<table role=\"presentation\" width=\"100%\"><tr><td align=\"center\"><table role=\"presentation\" width=\"100%\" style=\"max-width:560px;background:#fff;border:1px solid #dbeafe;border-radius:22px;overflow:hidden;box-shadow:0 14px 40px rgba(15,23,42,.08);\">\n<tr><td style=\"height:6px;background:linear-gradient(90deg,#0ea5e9,#2563eb,#f59e0b);\"></td></tr>\n<tr><td style=\"padding:30px 34px 14px;font-family:Arial,sans-serif;\"><div style=\"font-size:11px;font-weight:800;letter-spacing:.16em;color:#b45309;text-transform:uppercase;\">Secure access</div><div style=\"font-size:19px;font-weight:800;color:#0f172a;margin-top:4px;\">⚡ ${safeApp}</div></td></tr>\n<tr><td style=\"padding:10px 34px 8px;font-family:Arial,sans-serif;\"><h1 style=\"margin:0 0 12px;font-size:25px;color:#0f172a;\">Your account is ready</h1><p style=\"margin:0 0 8px;font-size:14px;line-height:22px;color:#334155;\">Hi ${safeName},</p><p style=\"margin:0;font-size:14px;line-height:22px;color:#475569;\">Your secure account was created successfully. Sign in with the email and password you registered.</p></td></tr>\n<tr><td align=\"center\" style=\"padding:24px 34px 28px;\"><a href=\"${safeUrl}\" style=\"display:inline-block;background:#0284c7;color:#fff;text-decoration:none;font-family:Arial,sans-serif;font-size:14px;font-weight:800;padding:13px 24px;border-radius:12px;\">Open audit dashboard</a></td></tr>\n<tr><td style=\"padding:19px 34px 26px;border-top:1px solid #e2e8f0;font-family:Arial,sans-serif;font-size:11px;line-height:18px;color:#64748b;\">If you did not create this account, contact the administrator.</td></tr>\n</table></td></tr></table></body></html>`;\nreturn [{ json: {\n shouldSend: true,\n to: request.email,\n subject,\n html,\n statusCode: 201,\n response: { ok: true, message: 'Account created successfully. You can sign in now.', nextStep: 'sign_in' }\n}}];"
},
"id": "4b436037-7bee-45f1-bc6e-efa7d94fd4f4",
"name": "Build Welcome Email",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
5888,
3216
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "09f816f2-c95a-4307-a36a-4e3d64289149",
"leftValue": "={{ $json.shouldSend }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "f5347fc5-5b58-499c-9538-20dde622ae9d",
"name": "Welcome Email Ready?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
6128,
3216
]
},
{
"parameters": {
"sendTo": "={{ $json.to }}",
"subject": "={{ $json.subject }}",
"message": "={{ $json.html }}",
"options": {
"appendAttribution": false,
"senderName": "George Mendieta"
}
},
"id": "9710a16c-d7cc-4da0-a563-cc6ad4a9cda3",
"name": "Send Welcome with Gmail",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
6368,
3136
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 2000,
"webhookId": "f3ed6ad7-4aed-498e-bc15-2544ec3a6250",
"credentials": {
"gmailOAuth2": {
"id": "UDcO1FLJqA453V2D",
"name": "Gmail account 3"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const prepared = $('Build Welcome Email').first().json;\nconst result = $input.first().json || {};\nconst failed = Boolean(result.error || result.errorMessage || result?.json?.error);\nreturn [{ json: {\n statusCode: 201,\n response: failed\n ? { ok: true, message: 'Account created successfully. You can sign in now. The welcome email could not be sent.', nextStep: 'sign_in' }\n : prepared.response\n}}];"
},
"id": "aec99d5b-f307-4cd9-8c2c-7eef0d5c4bf6",
"name": "Finalize Registration",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6608,
3136
]
},
{
"parameters": {
"method": "DELETE",
"url": "={{ $('CONFIG · Supabase').first().json.supabaseUrl.replace(/\\/$/, '') + '/auth/v1/admin/users/' + $json.userId }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"options": {
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "f76af2fc-4494-4608-a150-3c3453677dc0",
"name": "Roll Back Partial User",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
5888,
3456
]
},
{
"parameters": {
"jsCode": "const request = $('Prepare Access Grant').first().json;\nconst rollback = $input.first().json || {};\nconst rollbackStatus = Number(rollback.statusCode || rollback.status || 0);\nconst removed = rollbackStatus >= 200 && rollbackStatus < 300;\nreturn [{ json: {\n statusCode: 502,\n response: {\n ok: false,\n message: removed\n ? 'The account could not be completed, so the partial registration was removed. Please try again.'\n : 'The account could not be completed. Please contact the administrator before trying again.'\n },\n email: request.email\n}}];"
},
"id": "f2c9be31-d317-4e31-b5d1-a1de8d8b24a1",
"name": "Finalize Registration Failure",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6128,
3456
]
},
{
"parameters": {
"url": "={{ $('CONFIG · Supabase').first().json.supabaseUrl.replace(/\\/$/, '') + '/rest/v1/lucozade_access' }}",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "email",
"value": "={{ 'eq.' + $json.email }}"
},
{
"name": "is_active",
"value": "eq.true"
},
{
"name": "select",
"value": "user_id,email,full_name"
},
{
"name": "limit",
"value": "1"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"options": {
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "d07b7d52-57e3-4cb9-9924-53ad5e6dd4c8",
"name": "Find Active Lucozade Access",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
4624,
4304
]
},
{
"parameters": {
"jsCode": "const request = $('Validate Request').first().json;\nconst response = $input.first().json || {};\nconst hasEnvelope = Object.prototype.hasOwnProperty.call(response, 'statusCode') || Object.prototype.hasOwnProperty.call(response, 'body');\nconst statusCode = Number(response.statusCode || response.status || 0);\nconst body = response.body ?? response.data ?? response;\nconst records = Array.isArray(body) ? body : (body?.user_id ? [body] : []);\nconst apiOk = hasEnvelope ? statusCode >= 200 && statusCode < 300 : Array.isArray(records);\nconst record = records[0] || {};\nconst authorized = apiOk && Boolean(record.user_id);\nreturn [{ json: {\n ...request,\n recoveryAuthorized: authorized,\n fullName: String(record.full_name || '').trim(),\n statusCode: apiOk ? 200 : ([401,403].includes(statusCode) ? 500 : 502),\n response: authorized\n ? { ok: true, message: 'Recovery request accepted.' }\n : apiOk\n ? { ok: true, message: 'If an active account exists, a recovery link has been sent.' }\n : { ok: false, message: [401,403].includes(statusCode)\n ? 'The recovery service is not configured correctly.'\n : 'The recovery service is temporarily unavailable.' }\n}}];"
},
"id": "25f75686-7faa-4854-ac20-184fdf2b303c",
"name": "Prepare Recovery Access",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4864,
4304
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "c0f22b50-9e09-4d54-b513-bfbb98cc18cc",
"leftValue": "={{ $json.recoveryAuthorized }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "c4afa97c-7c30-4b68-abe5-9c00a9b38a52",
"name": "Recovery Authorized?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
5328,
4592
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $('CONFIG · Supabase').first().json.supabaseUrl.replace(/\\/$/, '') + '/auth/v1/admin/generate_link' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { type: 'recovery', email: $json.email, redirect_to: $json.redirectUrl } }}",
"options": {
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "6828e473-fa47-445a-89c6-a5c288eae0c0",
"name": "Generate Supabase Recovery Link",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
5808,
4256
]
},
{
"parameters": {
"jsCode": "const request = $('Prepare Recovery Access').first().json;\nconst response = $input.first().json || {};\nconst hasEnvelope = Object.prototype.hasOwnProperty.call(response, 'statusCode') || Object.prototype.hasOwnProperty.call(response, 'body');\nconst statusCode = Number(response.statusCode || response.status || 0);\nconst body = response.body ?? response.data ?? response;\nconst actionLink = String(\n body?.action_link || body?.properties?.action_link || body?.data?.action_link || body?.data?.properties?.action_link || ''\n).trim();\nconst apiOk = hasEnvelope ? statusCode >= 200 && statusCode < 300 : Boolean(actionLink);\nif (!apiOk || !actionLink) {\n return [{ json: {\n shouldSend: false,\n statusCode: [401,403].includes(statusCode) ? 500 : 502,\n response: { ok: false, message: 'The recovery email could not be prepared. Please try again.' }\n }}];\n}\nconst escapeHtml = (value) => String(value ?? '')\n .replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')\n .replaceAll('\"', '&quot;').replaceAll(\"'\", '&#039;');\nconst safeLink = escapeHtml(actionLink);\nconst safeName = request.fullName ? escapeHtml(request.fullName) : 'there';\nconst safeApp = escapeHtml(request.appName || 'Lucozade Store Audit');\nconst subject = 'Reset your Lucozade Store Audit password';\nconst html = `<!doctype html><html lang=\"en\"><body style=\"margin:0;background:#f4f8fb;padding:28px 12px;\">\n<table role=\"presentation\" width=\"100%\"><tr><td align=\"center\"><table role=\"presentation\" width=\"100%\" style=\"max-width:560px;background:#fff;border:1px solid #dbeafe;border-radius:22px;overflow:hidden;box-shadow:0 14px 40px rgba(15,23,42,.08);\">\n<tr><td style=\"height:6px;background:linear-gradient(90deg,#0ea5e9,#2563eb,#f59e0b);\"></td></tr>\n<tr><td style=\"padding:30px 34px 14px;font-family:Arial,sans-serif;\"><div style=\"font-size:11px;font-weight:800;letter-spacing:.16em;color:#b45309;text-transform:uppercase;\">Secure access</div><div style=\"font-size:19px;font-weight:800;color:#0f172a;margin-top:4px;\">⚡ ${safeApp}</div></td></tr>\n<tr><td style=\"padding:10px 34px 8px;font-family:Arial,sans-serif;\"><h1 style=\"margin:0 0 12px;font-size:25px;color:#0f172a;\">Reset your password</h1><p style=\"margin:0 0 8px;font-size:14px;line-height:22px;color:#334155;\">Hi ${safeName},</p><p style=\"margin:0;font-size:14px;line-height:22px;color:#475569;\">Use the secure button below to create a new password. The link is generated and verified by Supabase Auth.</p></td></tr>\n<tr><td align=\"center\" style=\"padding:24px 34px 26px;\"><a href=\"${safeLink}\" style=\"display:inline-block;background:#0284c7;color:#fff;text-decoration:none;font-family:Arial,sans-serif;font-size:14px;font-weight:800;padding:13px 24px;border-radius:12px;\">Create new password</a></td></tr>\n<tr><td style=\"padding:0 34px 22px;font-family:Arial,sans-serif;\"><p style=\"margin:0 0 7px;font-size:11px;line-height:17px;color:#64748b;\">If the button does not open, copy this link:</p><p style=\"margin:0;font-size:10px;line-height:16px;color:#0284c7;word-break:break-all;\">${safeLink}</p></td></tr>\n<tr><td style=\"padding:19px 34px 26px;border-top:1px solid #e2e8f0;font-family:Arial,sans-serif;font-size:11px;line-height:18px;color:#64748b;\">If you did not request a password reset, you can ignore this message.</td></tr>\n</table></td></tr></table></body></html>`;\nreturn [{ json: {\n shouldSend: true,\n to: request.email,\n subject,\n html,\n statusCode: 200,\n response: { ok: true, message: 'Recovery link sent. Check your email.' }\n}}];"
},
"id": "bb99a128-623f-4374-b4a1-dbc633402006",
"name": "Build Password Reset Email",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6048,
4256
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "13ddd7ba-ec0f-4c50-847a-abd888639aa8",
"leftValue": "={{ $json.shouldSend }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "7bb63607-732c-4f59-b5a1-9120379a6260",
"name": "Reset Email Ready?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
6288,
4256
]
},
{
"parameters": {
"sendTo": "={{ $json.to }}",
"subject": "={{ $json.subject }}",
"message": "={{ $json.html }}",
"options": {
"appendAttribution": false,
"senderName": "George Mendieta"
}
},
"id": "c10560e2-8c40-4fa8-9b48-51e02e85d296",
"name": "Send Reset with Gmail",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
6928,
4048
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 2000,
"webhookId": "9b0d851a-558d-4abf-85cc-908fe82e0004",
"credentials": {
"gmailOAuth2": {
"id": "UDcO1FLJqA453V2D",
"name": "Gmail account 3"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const prepared = $('Build Password Reset Email').first().json;\nconst result = $input.first().json || {};\nconst failed = Boolean(result.error || result.errorMessage || result?.json?.error);\nreturn [{ json: {\n statusCode: failed ? 502 : 200,\n response: failed\n ? { ok: false, message: 'The recovery email could not be sent. Please try again.' }\n : prepared.response\n}}];"
},
"id": "1aaabda9-328a-42c5-9771-da13e1b44744",
"name": "Finalize Recovery Email",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
7168,
4048
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json.response }}",
"options": {
"responseCode": "={{ $json.statusCode || 200 }}",
"responseHeaders": {
"entries": [
{
"name": "Cache-Control",
"value": "no-store"
},
{
"name": "X-Content-Type-Options",
"value": "nosniff"
}
]
}
}
},
"id": "79ab70db-f4c0-4bbc-a01f-f0eca6a12109",
"name": "Respond to Application",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
8192,
3920
]
},
{
"parameters": {
"content": "## 🔐 Entrada y clasificación\n\nRecibe la solicitud de autenticación, carga la configuración de Supabase, valida los datos recibidos y determina si corresponde ejecutar un registro o una recuperación de contraseña.",
"height": 400,
"width": 1328,
"color": 2
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
2592,
3584
],
"id": "247672ea-aaa9-453a-aa13-13ff8a9c9848",
"name": "Sticky Note"
},
{
"parameters": {
"content": "## 👤 Registro y acceso a Lucozade\n\nCrea el usuario en Supabase, registra su información, concede el acceso a Lucozade y envía el correo de bienvenida.\n\nSi alguna etapa falla, revierte el registro parcial y prepara una respuesta controlada de error.",
"height": 544,
"width": 2624,
"color": 5
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
4336,
3088
],
"id": "59367ad6-cc20-4b8a-9fb9-6e501bcc1798",
"name": "Sticky Note1"
},
{
"parameters": {
"content": "## 🔑 Recuperación y respuesta final\n\nVerifica que el usuario tenga acceso activo, genera el enlace seguro de recuperación, construye y envía el correo para restablecer la contraseña.\n\nFinalmente, devuelve a la aplicación el resultado producido por cualquiera de las rutas del flujo.",
"height": 976,
"width": 3904,
"color": 4
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
4560,
3776
],
"id": "84e3e3ca-b3de-44c8-b5bd-734161456ee2",
"name": "Sticky Note2"
}
],
"connections": {
"Lucozade Auth Webhook": {
"main": [
[
{
"node": "CONFIG · Supabase",
"type": "main",
"index": 0
}
]
]
},
"CONFIG · Supabase": {
"main": [
[
{
"node": "Validate Request",
"type": "main",
"index": 0
}
]
]
},
"Validate Request": {
"main": [
[
{
"node": "Request Valid?",
"type": "main",
"index": 0
}
]
]
},
"Request Valid?": {
"main": [
[
{
"node": "Register Request?",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond to Application",
"type": "main",
"index": 0
}
]
]
},
"Register Request?": {
"main": [
[
{
"node": "Create Supabase User",
"type": "main",
"index": 0
}
],
[
{
"node": "Find Active Lucozade Access",
"type": "main",
"index": 0
}
]
]
},
"Create Supabase User": {
"main": [
[
{
"node": "Prepare Registration",
"type": "main",
"index": 0
}
]
]
},
"Prepare Registration": {
"main": [
[
{
"node": "Registration Created?",
"type": "main",
"index": 0
}
]
]
},
"Registration Created?": {
"main": [
[
{
"node": "Grant Lucozade Access",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond to Application",
"type": "main",
"index": 0
}
]
]
},
"Grant Lucozade Access": {
"main": [
[
{
"node": "Prepare Access Grant",
"type": "main",
"index": 0
}
]
]
},
"Prepare Access Grant": {
"main": [
[
{
"node": "Access Granted?",
"type": "main",
"index": 0
}
]
]
},
"Access Granted?": {
"main": [
[
{
"node": "Build Welcome Email",
"type": "main",
"index": 0
}
],
[
{
"node": "Roll Back Partial User",
"type": "main",
"index": 0
}
]
]
},
"Build Welcome Email": {
"main": [
[
{
"node": "Welcome Email Ready?",
"type": "main",
"index": 0
}
]
]
},
"Welcome Email Ready?": {
"main": [
[
{
"node": "Send Welcome with Gmail",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond to Application",
"type": "main",
"index": 0
}
]
]
},
"Send Welcome with Gmail": {
"main": [
[
{
"node": "Finalize Registration",
"type": "main",
"index": 0
}
]
]
},
"Finalize Registration": {
"main": [
[
{
"node": "Respond to Application",
"type": "main",
"index": 0
}
]
]
},
"Roll Back Partial User": {
"main": [
[
{
"node": "Finalize Registration Failure",
"type": "main",
"index": 0
}
]
]
},
"Finalize Registration Failure": {
"main": [
[
{
"node": "Respond to Application",
"type": "main",
"index": 0
}
]
]
},
"Find Active Lucozade Access": {
"main": [
[
{
"node": "Prepare Recovery Access",
"type": "main",
"index": 0
}
]
]
},
"Prepare Recovery Access": {
"main": [
[
{
"node": "Recovery Authorized?",
"type": "main",
"index": 0
}
]
]
},
"Recovery Authorized?": {
"main": [
[
{
"node": "Generate Supabase Recovery Link",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond to Application",
"type": "main",
"index": 0
}
]
]
},
"Generate Supabase Recovery Link": {
"main": [
[
{
"node": "Build Password Reset Email",
"type": "main",
"index": 0
}
]
]
},
"Build Password Reset Email": {
"main": [
[
{
"node": "Reset Email Ready?",
"type": "main",
"index": 0
}
]
]
},
"Reset Email Ready?": {
"main": [
[
{
"node": "Send Reset with Gmail",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond to Application",
"type": "main",
"index": 0
}
]
]
},
"Send Reset with Gmail": {
"main": [
[
{
"node": "Finalize Recovery Email",
"type": "main",
"index": 0
}
]
]
},
"Finalize Recovery Email": {
"main": [
[
{
"node": "Respond to Application",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"binaryMode": "separate"
},
"staticData": {
"global": {
"authRateLimit": {
"148.0.79.249:register:iaracena@gomezleemarketing.com": [
1785961571040,
1785961572322,
1785961925334,
1785961926582,
1785961941592
],
"148.0.79.249:recover:iaracena@gomezleemarketing.com": [
1785962009186,
1785962010832,
1785962034123,
1785962042665
]
},
"lucozadeAuthRateLimitV4": {
"148.0.79.249:register:iaracena@gomezleemarketing.com": [
1785969461690,
1785969462905,
1785969464680,
1785969465164,
1785969465606,
1785969465996,
1785969466540,
1785969578923
]
},
"lucozadeAuthRateLimitV5": {
"148.0.79.249:recover:iaracena@gomezleemarketing.com": [
1785974473385,
1785974475131,
1785974656412
],
"148.0.79.249:register:iaracena@gomezleemarketing.com": [
1785974872111,
1785975093605
]
},
"lucozadeAuthRateLimitV6": {
"191.156.186.145:register:jmendieta@gomezleemarketing.com": [
1786038433888
]
}
}
},
"meta": {
"templateCredsSetupCompleted": true
},
"versionId": "e96003ae-8443-4568-aa3c-8ff74f5351c8",
"activeVersionId": "e96003ae-8443-4568-aa3c-8ff74f5351c8",
"versionCounter": 194,
"triggerCount": 1,
"shared": [
{
"updatedAt": "2026-08-05T19:32:17.429Z",
"createdAt": "2026-08-05T19:32:17.429Z",
"role": "workflow:owner",
"workflowId": "WhKNJq6Dsu9Gmii7",
"projectId": "PJpTANzTXIFibWsW",
"project": {
"updatedAt": "2026-04-22T14:25:09.686Z",
"createdAt": "2026-04-22T14:22:54.790Z",
"id": "PJpTANzTXIFibWsW",
"name": "Isaac Aracena <iaracena@gomezleemarketing.com>",
"type": "personal",
"icon": null,
"description": null,
"creatorId": "0a88c0b1-928e-4412-896e-c5d1c99b2029"
}
}
],
"tags": [],
"activeVersion": {
"updatedAt": "2026-08-06T18:19:47.000Z",
"createdAt": "2026-08-06T18:19:46.434Z",
"versionId": "e96003ae-8443-4568-aa3c-8ff74f5351c8",
"workflowId": "WhKNJq6Dsu9Gmii7",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "lucozade-auth-v6",
"responseMode": "responseNode",
"options": {
"allowedOrigins": "https://digitalcompass.agency,http://localhost,http://127.0.0.1,http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173,http://127.0.0.1:5173,http://localhost:4173,http://127.0.0.1:4173"
}
},
"id": "3c56f534-1566-44ea-aca4-057e52576aa5",
"name": "Lucozade Auth Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
2704,
3808
],
"webhookId": "07ae0590-c2f5-43e5-94ea-903d7b74f3ec"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "7ec43804-056e-4289-afc6-f4c92a178118",
"name": "supabaseUrl",
"value": "https://dbit.digitalcompass.agency",
"type": "string"
},
{
"id": "0e5dd6d0-f0a4-4fe1-a515-51d74eb6446e",
"name": "serviceRoleKey",
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q",
"type": "string"
},
{
"id": "813d005b-5e39-42e1-b79a-18ef997d2632",
"name": "productionAppUrl",
"value": "https://digitalcompass.agency/lucozade/",
"type": "string"
},
{
"id": "e02a637e-4042-4f26-8810-18ece84142a7",
"name": "appName",
"value": "Lucozade Store Audit",
"type": "string"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "3c7977cc-17b4-4358-a264-837067faabcb",
"name": "CONFIG · Supabase",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
2944,
3808
]
},
{
"parameters": {
"jsCode": "const source = $input.first().json || {};\nconst body = source.body || {};\nconst headers = Object.fromEntries(\n Object.entries(source.headers || {}).map(([key, value]) => [String(key).toLowerCase(), String(value ?? '')])\n);\n\nconst action = String(body.action || '').trim().toLowerCase();\nconst email = String(body.email || '').trim().toLowerCase();\nconst fullName = String(body.fullName || '').trim();\nconst password = String(body.password || '');\nconst honeypot = String(body.website || '').trim();\nconst requestedRedirect = String(body.redirectUrl || '').trim();\nconst ip = String(headers['x-forwarded-for'] || headers['x-real-ip'] || 'unknown').split(',')[0].trim();\n\nconst config = $('CONFIG · Supabase').first().json || {};\nconst supabaseUrl = String(config.supabaseUrl || '').replace(/\\/$/, '');\nconst productionAppUrl = String(config.productionAppUrl || '').trim();\nconst appName = String(config.appName || 'Lucozade Store Audit').trim();\nconst serviceRoleKey = String(config.serviceRoleKey || '').trim();\n\nconst allowedRedirects = [\n 'https://digitalcompass.agency/lucozade/',\n 'http://localhost:3000/',\n 'http://127.0.0.1:3000/',\n 'http://localhost:5173/',\n 'http://127.0.0.1:5173/',\n 'http://localhost:4173/lucozade/',\n 'http://127.0.0.1:4173/lucozade/'\n];\nconst redirectUrl = allowedRedirects.includes(requestedRedirect) ? requestedRedirect : productionAppUrl;\n\nlet valid = true;\nlet statusCode = 400;\nlet message = '';\n\nif (!supabaseUrl || !productionAppUrl || !serviceRoleKey || serviceRoleKey.includes('PASTE_')) {\n valid = false;\n statusCode = 500;\n message = 'The authentication service is not configured.';\n} else if (honeypot) {\n valid = false;\n message = 'Invalid request.';\n} else if (!['register', 'recover'].includes(action)) {\n valid = false;\n message = 'Invalid authentication action.';\n} else if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email) || email.length > 254) {\n valid = false;\n message = 'Enter a valid email address.';\n} else if (action === 'register' && (fullName.length < 2 || fullName.length > 120)) {\n valid = false;\n message = 'Enter your name.';\n} else if (action === 'register' && (password.length < 8 || password.length > 72)) {\n valid = false;\n message = 'Use a password between 8 and 72 characters.';\n}\n\nif (valid) {\n const staticData = $getWorkflowStaticData('global');\n staticData.lucozadeAuthRateLimitV6 = staticData.lucozadeAuthRateLimitV6 || {};\n const now = Date.now();\n const windowMs = 15 * 60 * 1000;\n const key = `${ip}:${action}:${email}`;\n const attempts = (staticData.lucozadeAuthRateLimitV6[key] || []).filter((time) => now - Number(time) < windowMs);\n\n if (attempts.length >= 10) {\n valid = false;\n statusCode = 429;\n message = 'Too many attempts. Please wait 15 minutes and try again.';\n } else {\n attempts.push(now);\n staticData.lucozadeAuthRateLimitV6[key] = attempts;\n }\n\n for (const [storedKey, times] of Object.entries(staticData.lucozadeAuthRateLimitV6)) {\n const recent = Array.isArray(times) ? times.filter((time) => now - Number(time) < windowMs) : [];\n if (recent.length) staticData.lucozadeAuthRateLimitV6[storedKey] = recent;\n else delete staticData.lucozadeAuthRateLimitV6[storedKey];\n }\n}\n\nreturn [{ json: {\n valid,\n statusCode,\n response: { ok: valid, message: valid ? 'Request accepted.' : message },\n action,\n email,\n fullName,\n password,\n redirectUrl,\n appName,\n supabaseUrl,\n ip\n}}];"
},
"id": "df0717c9-4552-462a-9112-5cb87f033aa9",
"name": "Validate Request",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3184,
3808
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "a5e66cca-1fa1-420c-9f6a-2852ce2ebcec",
"leftValue": "={{ $json.valid }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "90488719-d6c7-4b08-a586-c9170fd37c37",
"name": "Request Valid?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
3424,
3808
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "6216684f-1892-4ad4-b321-3ff29f872514",
"leftValue": "={{ $json.action }}",
"rightValue": "register",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "ea555e17-eba1-43ab-832a-c7f7f8bb8c8f",
"name": "Register Request?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
3664,
3696
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $('CONFIG · Supabase').first().json.supabaseUrl.replace(/\\/$/, '') + '/auth/v1/admin/users' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { email: $json.email, password: $json.password, email_confirm: true, user_metadata: { full_name: $json.fullName, app_id: 'lucozade-audit' } } }}",
"options": {
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "4ea8c30e-e72f-4dfe-8df9-1730321276ec",
"name": "Create Supabase User",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
4448,
3408
]
},
{
"parameters": {
"jsCode": "const request = $('Validate Request').first().json;\nconst response = $input.first().json || {};\nconst hasEnvelope = Object.prototype.hasOwnProperty.call(response, 'statusCode') || Object.prototype.hasOwnProperty.call(response, 'body');\nconst statusCode = Number(response.statusCode || response.status || 0);\nconst body = response.body ?? response.data ?? response;\nconst user = body?.user || body || {};\nconst userId = String(user?.id || '').trim();\nconst apiOk = hasEnvelope ? statusCode >= 200 && statusCode < 300 : Boolean(userId);\nconst errorText = [body?.message, body?.msg, body?.error, body?.error_description, body?.code, body?.error_code]\n .filter(Boolean).join(' ');\nconst duplicate = /already.*registered|already exists|user.*exists|email_exists|user_already_exists|duplicate/i.test(errorText);\n\nreturn [{ json: {\n ...request,\n registrationCreated: apiOk && Boolean(userId),\n userId,\n statusCode: apiOk && userId ? 201 : (duplicate ? 409 : ([401,403].includes(statusCode) ? 500 : 502)),\n response: apiOk && userId\n ? { ok: true, message: 'Account created successfully.' }\n : duplicate\n ? { ok: false, message: 'An account already exists for this email. Use Sign in or Forgot password.' }\n : { ok: false, message: [401,403].includes(statusCode)\n ? 'The authentication service is not configured correctly.'\n : 'The account could not be created. Please try again.' }\n}}];"
},
"id": "0d76bf29-ed1b-4cdc-831e-102b61ee96a7",
"name": "Prepare Registration",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4688,
3408
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "ed820bef-c9f1-4d58-a327-ef60975043d8",
"leftValue": "={{ $json.registrationCreated }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "956fd0d2-914c-4a93-b729-2211cb8c031c",
"name": "Registration Created?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
4928,
3408
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $('CONFIG · Supabase').first().json.supabaseUrl.replace(/\\/$/, '') + '/rest/v1/lucozade_access?on_conflict=user_id' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "resolution=merge-duplicates,return=representation"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { user_id: $json.userId, email: $json.email, full_name: $json.fullName, is_active: true } }}",
"options": {
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "82a01c0a-ca12-41ab-a29e-6614b6ff7f7c",
"name": "Grant Lucozade Access",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
5168,
3312
]
},
{
"parameters": {
"jsCode": "const request = $('Prepare Registration').first().json;\nconst response = $input.first().json || {};\nconst hasEnvelope = Object.prototype.hasOwnProperty.call(response, 'statusCode') || Object.prototype.hasOwnProperty.call(response, 'body');\nconst statusCode = Number(response.statusCode || response.status || 0);\nconst body = response.body ?? response.data ?? response;\nconst accessGranted = hasEnvelope ? statusCode >= 200 && statusCode < 300 : Array.isArray(body);\nreturn [{ json: {\n ...request,\n accessGranted,\n statusCode: accessGranted ? 201 : 502,\n response: accessGranted\n ? { ok: true, message: 'Account created successfully.' }\n : { ok: false, message: 'The account could not be completed. Please try again.' }\n}}];"
},
"id": "be8d79f3-ab97-4aba-9642-7eb3910d5e02",
"name": "Prepare Access Grant",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
5408,
3312
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "e4dbcaf7-fc7a-47d9-a275-f8d81c669710",
"leftValue": "={{ $json.accessGranted }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "9d2ff8a2-bd1d-48ec-ab32-459b989d8d39",
"name": "Access Granted?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
5648,
3312
]
},
{
"parameters": {
"jsCode": "const request = $('Prepare Access Grant').first().json;\nconst escapeHtml = (value) => String(value ?? '')\n .replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')\n .replaceAll('\"', '&quot;').replaceAll(\"'\", '&#039;');\nconst safeName = escapeHtml(request.fullName || 'there');\nconst safeApp = escapeHtml(request.appName || 'Lucozade Store Audit');\nconst safeUrl = escapeHtml(request.redirectUrl);\nconst subject = 'Your Lucozade Store Audit account is ready';\nconst html = `<!doctype html><html lang=\"en\"><body style=\"margin:0;background:#f4f8fb;padding:28px 12px;\">\n<table role=\"presentation\" width=\"100%\"><tr><td align=\"center\"><table role=\"presentation\" width=\"100%\" style=\"max-width:560px;background:#fff;border:1px solid #dbeafe;border-radius:22px;overflow:hidden;box-shadow:0 14px 40px rgba(15,23,42,.08);\">\n<tr><td style=\"height:6px;background:linear-gradient(90deg,#0ea5e9,#2563eb,#f59e0b);\"></td></tr>\n<tr><td style=\"padding:30px 34px 14px;font-family:Arial,sans-serif;\"><div style=\"font-size:11px;font-weight:800;letter-spacing:.16em;color:#b45309;text-transform:uppercase;\">Secure access</div><div style=\"font-size:19px;font-weight:800;color:#0f172a;margin-top:4px;\">⚡ ${safeApp}</div></td></tr>\n<tr><td style=\"padding:10px 34px 8px;font-family:Arial,sans-serif;\"><h1 style=\"margin:0 0 12px;font-size:25px;color:#0f172a;\">Your account is ready</h1><p style=\"margin:0 0 8px;font-size:14px;line-height:22px;color:#334155;\">Hi ${safeName},</p><p style=\"margin:0;font-size:14px;line-height:22px;color:#475569;\">Your secure account was created successfully. Sign in with the email and password you registered.</p></td></tr>\n<tr><td align=\"center\" style=\"padding:24px 34px 28px;\"><a href=\"${safeUrl}\" style=\"display:inline-block;background:#0284c7;color:#fff;text-decoration:none;font-family:Arial,sans-serif;font-size:14px;font-weight:800;padding:13px 24px;border-radius:12px;\">Open audit dashboard</a></td></tr>\n<tr><td style=\"padding:19px 34px 26px;border-top:1px solid #e2e8f0;font-family:Arial,sans-serif;font-size:11px;line-height:18px;color:#64748b;\">If you did not create this account, contact the administrator.</td></tr>\n</table></td></tr></table></body></html>`;\nreturn [{ json: {\n shouldSend: true,\n to: request.email,\n subject,\n html,\n statusCode: 201,\n response: { ok: true, message: 'Account created successfully. You can sign in now.', nextStep: 'sign_in' }\n}}];"
},
"id": "4b436037-7bee-45f1-bc6e-efa7d94fd4f4",
"name": "Build Welcome Email",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
5888,
3216
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "09f816f2-c95a-4307-a36a-4e3d64289149",
"leftValue": "={{ $json.shouldSend }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "f5347fc5-5b58-499c-9538-20dde622ae9d",
"name": "Welcome Email Ready?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
6128,
3216
]
},
{
"parameters": {
"sendTo": "={{ $json.to }}",
"subject": "={{ $json.subject }}",
"message": "={{ $json.html }}",
"options": {
"appendAttribution": false,
"senderName": "George Mendieta"
}
},
"id": "9710a16c-d7cc-4da0-a563-cc6ad4a9cda3",
"name": "Send Welcome with Gmail",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
6368,
3136
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 2000,
"webhookId": "f3ed6ad7-4aed-498e-bc15-2544ec3a6250",
"credentials": {
"gmailOAuth2": {
"id": "UDcO1FLJqA453V2D",
"name": "Gmail account 3"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const prepared = $('Build Welcome Email').first().json;\nconst result = $input.first().json || {};\nconst failed = Boolean(result.error || result.errorMessage || result?.json?.error);\nreturn [{ json: {\n statusCode: 201,\n response: failed\n ? { ok: true, message: 'Account created successfully. You can sign in now. The welcome email could not be sent.', nextStep: 'sign_in' }\n : prepared.response\n}}];"
},
"id": "aec99d5b-f307-4cd9-8c2c-7eef0d5c4bf6",
"name": "Finalize Registration",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6608,
3136
]
},
{
"parameters": {
"method": "DELETE",
"url": "={{ $('CONFIG · Supabase').first().json.supabaseUrl.replace(/\\/$/, '') + '/auth/v1/admin/users/' + $json.userId }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"options": {
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "f76af2fc-4494-4608-a150-3c3453677dc0",
"name": "Roll Back Partial User",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
5888,
3456
]
},
{
"parameters": {
"jsCode": "const request = $('Prepare Access Grant').first().json;\nconst rollback = $input.first().json || {};\nconst rollbackStatus = Number(rollback.statusCode || rollback.status || 0);\nconst removed = rollbackStatus >= 200 && rollbackStatus < 300;\nreturn [{ json: {\n statusCode: 502,\n response: {\n ok: false,\n message: removed\n ? 'The account could not be completed, so the partial registration was removed. Please try again.'\n : 'The account could not be completed. Please contact the administrator before trying again.'\n },\n email: request.email\n}}];"
},
"id": "f2c9be31-d317-4e31-b5d1-a1de8d8b24a1",
"name": "Finalize Registration Failure",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6128,
3456
]
},
{
"parameters": {
"url": "={{ $('CONFIG · Supabase').first().json.supabaseUrl.replace(/\\/$/, '') + '/rest/v1/lucozade_access' }}",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "email",
"value": "={{ 'eq.' + $json.email }}"
},
{
"name": "is_active",
"value": "eq.true"
},
{
"name": "select",
"value": "user_id,email,full_name"
},
{
"name": "limit",
"value": "1"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"options": {
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "d07b7d52-57e3-4cb9-9924-53ad5e6dd4c8",
"name": "Find Active Lucozade Access",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
4624,
4304
]
},
{
"parameters": {
"jsCode": "const request = $('Validate Request').first().json;\nconst response = $input.first().json || {};\nconst hasEnvelope = Object.prototype.hasOwnProperty.call(response, 'statusCode') || Object.prototype.hasOwnProperty.call(response, 'body');\nconst statusCode = Number(response.statusCode || response.status || 0);\nconst body = response.body ?? response.data ?? response;\nconst records = Array.isArray(body) ? body : (body?.user_id ? [body] : []);\nconst apiOk = hasEnvelope ? statusCode >= 200 && statusCode < 300 : Array.isArray(records);\nconst record = records[0] || {};\nconst authorized = apiOk && Boolean(record.user_id);\nreturn [{ json: {\n ...request,\n recoveryAuthorized: authorized,\n fullName: String(record.full_name || '').trim(),\n statusCode: apiOk ? 200 : ([401,403].includes(statusCode) ? 500 : 502),\n response: authorized\n ? { ok: true, message: 'Recovery request accepted.' }\n : apiOk\n ? { ok: true, message: 'If an active account exists, a recovery link has been sent.' }\n : { ok: false, message: [401,403].includes(statusCode)\n ? 'The recovery service is not configured correctly.'\n : 'The recovery service is temporarily unavailable.' }\n}}];"
},
"id": "25f75686-7faa-4854-ac20-184fdf2b303c",
"name": "Prepare Recovery Access",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4864,
4304
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "c0f22b50-9e09-4d54-b513-bfbb98cc18cc",
"leftValue": "={{ $json.recoveryAuthorized }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "c4afa97c-7c30-4b68-abe5-9c00a9b38a52",
"name": "Recovery Authorized?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
5328,
4592
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $('CONFIG · Supabase').first().json.supabaseUrl.replace(/\\/$/, '') + '/auth/v1/admin/generate_link' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $('CONFIG · Supabase').first().json.serviceRoleKey }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { type: 'recovery', email: $json.email, redirect_to: $json.redirectUrl } }}",
"options": {
"response": {
"response": {
"fullResponse": true,
"neverError": true,
"responseFormat": "json"
}
},
"timeout": 30000
}
},
"id": "6828e473-fa47-445a-89c6-a5c288eae0c0",
"name": "Generate Supabase Recovery Link",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
5808,
4256
]
},
{
"parameters": {
"jsCode": "const request = $('Prepare Recovery Access').first().json;\nconst response = $input.first().json || {};\nconst hasEnvelope = Object.prototype.hasOwnProperty.call(response, 'statusCode') || Object.prototype.hasOwnProperty.call(response, 'body');\nconst statusCode = Number(response.statusCode || response.status || 0);\nconst body = response.body ?? response.data ?? response;\nconst actionLink = String(\n body?.action_link || body?.properties?.action_link || body?.data?.action_link || body?.data?.properties?.action_link || ''\n).trim();\nconst apiOk = hasEnvelope ? statusCode >= 200 && statusCode < 300 : Boolean(actionLink);\nif (!apiOk || !actionLink) {\n return [{ json: {\n shouldSend: false,\n statusCode: [401,403].includes(statusCode) ? 500 : 502,\n response: { ok: false, message: 'The recovery email could not be prepared. Please try again.' }\n }}];\n}\nconst escapeHtml = (value) => String(value ?? '')\n .replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;')\n .replaceAll('\"', '&quot;').replaceAll(\"'\", '&#039;');\nconst safeLink = escapeHtml(actionLink);\nconst safeName = request.fullName ? escapeHtml(request.fullName) : 'there';\nconst safeApp = escapeHtml(request.appName || 'Lucozade Store Audit');\nconst subject = 'Reset your Lucozade Store Audit password';\nconst html = `<!doctype html><html lang=\"en\"><body style=\"margin:0;background:#f4f8fb;padding:28px 12px;\">\n<table role=\"presentation\" width=\"100%\"><tr><td align=\"center\"><table role=\"presentation\" width=\"100%\" style=\"max-width:560px;background:#fff;border:1px solid #dbeafe;border-radius:22px;overflow:hidden;box-shadow:0 14px 40px rgba(15,23,42,.08);\">\n<tr><td style=\"height:6px;background:linear-gradient(90deg,#0ea5e9,#2563eb,#f59e0b);\"></td></tr>\n<tr><td style=\"padding:30px 34px 14px;font-family:Arial,sans-serif;\"><div style=\"font-size:11px;font-weight:800;letter-spacing:.16em;color:#b45309;text-transform:uppercase;\">Secure access</div><div style=\"font-size:19px;font-weight:800;color:#0f172a;margin-top:4px;\">⚡ ${safeApp}</div></td></tr>\n<tr><td style=\"padding:10px 34px 8px;font-family:Arial,sans-serif;\"><h1 style=\"margin:0 0 12px;font-size:25px;color:#0f172a;\">Reset your password</h1><p style=\"margin:0 0 8px;font-size:14px;line-height:22px;color:#334155;\">Hi ${safeName},</p><p style=\"margin:0;font-size:14px;line-height:22px;color:#475569;\">Use the secure button below to create a new password. The link is generated and verified by Supabase Auth.</p></td></tr>\n<tr><td align=\"center\" style=\"padding:24px 34px 26px;\"><a href=\"${safeLink}\" style=\"display:inline-block;background:#0284c7;color:#fff;text-decoration:none;font-family:Arial,sans-serif;font-size:14px;font-weight:800;padding:13px 24px;border-radius:12px;\">Create new password</a></td></tr>\n<tr><td style=\"padding:0 34px 22px;font-family:Arial,sans-serif;\"><p style=\"margin:0 0 7px;font-size:11px;line-height:17px;color:#64748b;\">If the button does not open, copy this link:</p><p style=\"margin:0;font-size:10px;line-height:16px;color:#0284c7;word-break:break-all;\">${safeLink}</p></td></tr>\n<tr><td style=\"padding:19px 34px 26px;border-top:1px solid #e2e8f0;font-family:Arial,sans-serif;font-size:11px;line-height:18px;color:#64748b;\">If you did not request a password reset, you can ignore this message.</td></tr>\n</table></td></tr></table></body></html>`;\nreturn [{ json: {\n shouldSend: true,\n to: request.email,\n subject,\n html,\n statusCode: 200,\n response: { ok: true, message: 'Recovery link sent. Check your email.' }\n}}];"
},
"id": "bb99a128-623f-4374-b4a1-dbc633402006",
"name": "Build Password Reset Email",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
6048,
4256
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "13ddd7ba-ec0f-4c50-847a-abd888639aa8",
"leftValue": "={{ $json.shouldSend }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "7bb63607-732c-4f59-b5a1-9120379a6260",
"name": "Reset Email Ready?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
6288,
4256
]
},
{
"parameters": {
"sendTo": "={{ $json.to }}",
"subject": "={{ $json.subject }}",
"message": "={{ $json.html }}",
"options": {
"appendAttribution": false,
"senderName": "George Mendieta"
}
},
"id": "c10560e2-8c40-4fa8-9b48-51e02e85d296",
"name": "Send Reset with Gmail",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
6928,
4048
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 2000,
"webhookId": "9b0d851a-558d-4abf-85cc-908fe82e0004",
"credentials": {
"gmailOAuth2": {
"id": "UDcO1FLJqA453V2D",
"name": "Gmail account 3"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "const prepared = $('Build Password Reset Email').first().json;\nconst result = $input.first().json || {};\nconst failed = Boolean(result.error || result.errorMessage || result?.json?.error);\nreturn [{ json: {\n statusCode: failed ? 502 : 200,\n response: failed\n ? { ok: false, message: 'The recovery email could not be sent. Please try again.' }\n : prepared.response\n}}];"
},
"id": "1aaabda9-328a-42c5-9771-da13e1b44744",
"name": "Finalize Recovery Email",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
7168,
4048
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json.response }}",
"options": {
"responseCode": "={{ $json.statusCode || 200 }}",
"responseHeaders": {
"entries": [
{
"name": "Cache-Control",
"value": "no-store"
},
{
"name": "X-Content-Type-Options",
"value": "nosniff"
}
]
}
}
},
"id": "79ab70db-f4c0-4bbc-a01f-f0eca6a12109",
"name": "Respond to Application",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
8192,
3920
]
},
{
"parameters": {
"content": "## 🔐 Entrada y clasificación\n\nRecibe la solicitud de autenticación, carga la configuración de Supabase, valida los datos recibidos y determina si corresponde ejecutar un registro o una recuperación de contraseña.",
"height": 400,
"width": 1328,
"color": 2
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
2592,
3584
],
"id": "247672ea-aaa9-453a-aa13-13ff8a9c9848",
"name": "Sticky Note"
},
{
"parameters": {
"content": "## 👤 Registro y acceso a Lucozade\n\nCrea el usuario en Supabase, registra su información, concede el acceso a Lucozade y envía el correo de bienvenida.\n\nSi alguna etapa falla, revierte el registro parcial y prepara una respuesta controlada de error.",
"height": 544,
"width": 2624,
"color": 5
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
4336,
3088
],
"id": "59367ad6-cc20-4b8a-9fb9-6e501bcc1798",
"name": "Sticky Note1"
},
{
"parameters": {
"content": "## 🔑 Recuperación y respuesta final\n\nVerifica que el usuario tenga acceso activo, genera el enlace seguro de recuperación, construye y envía el correo para restablecer la contraseña.\n\nFinalmente, devuelve a la aplicación el resultado producido por cualquiera de las rutas del flujo.",
"height": 976,
"width": 3904,
"color": 4
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
4560,
3776
],
"id": "84e3e3ca-b3de-44c8-b5bd-734161456ee2",
"name": "Sticky Note2"
}
],
"connections": {
"Lucozade Auth Webhook": {
"main": [
[
{
"node": "CONFIG · Supabase",
"type": "main",
"index": 0
}
]
]
},
"CONFIG · Supabase": {
"main": [
[
{
"node": "Validate Request",
"type": "main",
"index": 0
}
]
]
},
"Validate Request": {
"main": [
[
{
"node": "Request Valid?",
"type": "main",
"index": 0
}
]
]
},
"Request Valid?": {
"main": [
[
{
"node": "Register Request?",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond to Application",
"type": "main",
"index": 0
}
]
]
},
"Register Request?": {
"main": [
[
{
"node": "Create Supabase User",
"type": "main",
"index": 0
}
],
[
{
"node": "Find Active Lucozade Access",
"type": "main",
"index": 0
}
]
]
},
"Create Supabase User": {
"main": [
[
{
"node": "Prepare Registration",
"type": "main",
"index": 0
}
]
]
},
"Prepare Registration": {
"main": [
[
{
"node": "Registration Created?",
"type": "main",
"index": 0
}
]
]
},
"Registration Created?": {
"main": [
[
{
"node": "Grant Lucozade Access",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond to Application",
"type": "main",
"index": 0
}
]
]
},
"Grant Lucozade Access": {
"main": [
[
{
"node": "Prepare Access Grant",
"type": "main",
"index": 0
}
]
]
},
"Prepare Access Grant": {
"main": [
[
{
"node": "Access Granted?",
"type": "main",
"index": 0
}
]
]
},
"Access Granted?": {
"main": [
[
{
"node": "Build Welcome Email",
"type": "main",
"index": 0
}
],
[
{
"node": "Roll Back Partial User",
"type": "main",
"index": 0
}
]
]
},
"Build Welcome Email": {
"main": [
[
{
"node": "Welcome Email Ready?",
"type": "main",
"index": 0
}
]
]
},
"Welcome Email Ready?": {
"main": [
[
{
"node": "Send Welcome with Gmail",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond to Application",
"type": "main",
"index": 0
}
]
]
},
"Send Welcome with Gmail": {
"main": [
[
{
"node": "Finalize Registration",
"type": "main",
"index": 0
}
]
]
},
"Finalize Registration": {
"main": [
[
{
"node": "Respond to Application",
"type": "main",
"index": 0
}
]
]
},
"Roll Back Partial User": {
"main": [
[
{
"node": "Finalize Registration Failure",
"type": "main",
"index": 0
}
]
]
},
"Finalize Registration Failure": {
"main": [
[
{
"node": "Respond to Application",
"type": "main",
"index": 0
}
]
]
},
"Find Active Lucozade Access": {
"main": [
[
{
"node": "Prepare Recovery Access",
"type": "main",
"index": 0
}
]
]
},
"Prepare Recovery Access": {
"main": [
[
{
"node": "Recovery Authorized?",
"type": "main",
"index": 0
}
]
]
},
"Recovery Authorized?": {
"main": [
[
{
"node": "Generate Supabase Recovery Link",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond to Application",
"type": "main",
"index": 0
}
]
]
},
"Generate Supabase Recovery Link": {
"main": [
[
{
"node": "Build Password Reset Email",
"type": "main",
"index": 0
}
]
]
},
"Build Password Reset Email": {
"main": [
[
{
"node": "Reset Email Ready?",
"type": "main",
"index": 0
}
]
]
},
"Reset Email Ready?": {
"main": [
[
{
"node": "Send Reset with Gmail",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond to Application",
"type": "main",
"index": 0
}
]
]
},
"Send Reset with Gmail": {
"main": [
[
{
"node": "Finalize Recovery Email",
"type": "main",
"index": 0
}
]
]
},
"Finalize Recovery Email": {
"main": [
[
{
"node": "Respond to Application",
"type": "main",
"index": 0
}
]
]
}
},
"authors": "Isaac Aracena",
"name": "Version e96003ae",
"description": "",
"autosaved": false,
"workflowPublishHistory": [
{
"createdAt": "2026-08-06T18:19:47.497Z",
"id": 4253,
"workflowId": "WhKNJq6Dsu9Gmii7",
"versionId": "e96003ae-8443-4568-aa3c-8ff74f5351c8",
"event": "activated",
"userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029"
}
]
}
}