diff --git a/lucozade-audit-dashboard.json b/lucozade-audit-dashboard.json
new file mode 100644
index 0000000..58b0c06
--- /dev/null
+++ b/lucozade-audit-dashboard.json
@@ -0,0 +1,2343 @@
+{
+ "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('&', '&').replaceAll('<', '<').replaceAll('>', '>')\n .replaceAll('\"', '"').replaceAll(\"'\", ''');\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 = `
\n\n | \nSecure access ⚡ ${safeApp} | \nYour account is readyHi ${safeName}, Your secure account was created successfully. Sign in with the email and password you registered. | \n| Open audit dashboard | \n| If you did not create this account, contact the administrator. | \n |
`;\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('&', '&').replaceAll('<', '<').replaceAll('>', '>')\n .replaceAll('\"', '"').replaceAll(\"'\", ''');\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 = `\n\n | \nSecure access ⚡ ${safeApp} | \nReset your passwordHi ${safeName}, Use the secure button below to create a new password. The link is generated and verified by Supabase Auth. | \n| Create new password | \nIf the button does not open, copy this link: ${safeLink} | \n| If you did not request a password reset, you can ignore this message. | \n |
`;\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 ",
+ "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('&', '&').replaceAll('<', '<').replaceAll('>', '>')\n .replaceAll('\"', '"').replaceAll(\"'\", ''');\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 = `\n\n | \nSecure access ⚡ ${safeApp} | \nYour account is readyHi ${safeName}, Your secure account was created successfully. Sign in with the email and password you registered. | \n| Open audit dashboard | \n| If you did not create this account, contact the administrator. | \n |
`;\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('&', '&').replaceAll('<', '<').replaceAll('>', '>')\n .replaceAll('\"', '"').replaceAll(\"'\", ''');\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 = `\n\n | \nSecure access ⚡ ${safeApp} | \nReset your passwordHi ${safeName}, Use the secure button below to create a new password. The link is generated and verified by Supabase Auth. | \n| Create new password | \nIf the button does not open, copy this link: ${safeLink} | \n| If you did not request a password reset, you can ignore this message. | \n |
`;\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"
+ }
+ ]
+ }
+}
\ No newline at end of file