{ "name": "Lucozade Audit Dashboard", "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\n\n\n\n\n
Secure access
⚡ ${safeApp}

Your account is ready

Hi ${safeName},

Your secure account was created successfully. Sign in with the email and password you registered.

Open audit dashboard
If you did not create this account, contact the administrator.
`;\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\n\n\n\n\n\n
Secure access
⚡ ${safeApp}

Reset your password

Hi ${safeName},

Use the secure button below to create a new password. The link is generated and verified by Supabase Auth.

Create new password

If the button does not open, copy this link:

${safeLink}

If you did not request a password reset, you can ignore this message.
`;\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" } ], "pinData": {}, "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 } ] ] } }, "active": true, "settings": { "executionOrder": "v1", "binaryMode": "separate" }, "versionId": "e96003ae-8443-4568-aa3c-8ff74f5351c8", "meta": { "templateCredsSetupCompleted": true, "instanceId": "b4b77b17af092830e794eef639ce2f6d7daccf7eddc075060b03b3b6545aac70" }, "id": "WhKNJq6Dsu9Gmii7", "tags": [] }