diff --git a/Lucozade Audit Dashboard.json b/Lucozade Audit Dashboard.json new file mode 100644 index 0000000..5580252 --- /dev/null +++ b/Lucozade Audit Dashboard.json @@ -0,0 +1,1134 @@ +{ + "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": [] +} \ No newline at end of file