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