{ "name": "W1 - Customer Provision (AList User + Role)", "nodes": [ { "parameters": { "httpMethod": "POST", "path": "alist-customer-provision", "responseMode": "onReceived", "options": {} }, "id": "trigger-webhook", "name": "NocoDB: Customer Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "position": [ 0, 0 ], "webhookId": "customer-provision" }, { "parameters": { "jsCode": "const body = $input.item.json.body ?? $input.item.json;\nconst data = body.data ?? {};\nconst rows = data.rows?.[0] ?? data.previous_rows?.[0] ?? data.rows_new?.[0] ?? data.rows_old?.[0] ?? {};\nconst id = rows.Id ?? data.data?.rows?.[0]?.Id;\nif (!id) throw new Error('no customer Id in webhook');\nreturn { json: { customerId: Number(id), eventType: body.type ?? '' } };\n" }, "id": "extract-id", "name": "Extract Customer Record Id", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 220, 0 ] }, { "parameters": { "jsCode": "// Step 2: read ALL env via $env (works in Code node), and pass through\n// the webhook-derived fields so downstream keeps customerId.\nconst prev = $input.item.json;\nreturn { json: {\n customerId: prev.customerId,\n eventType: prev.eventType,\n NOCODB_URL: $env.NOCODB_URL,\n NOCODB_TOKEN: $env.NOCODB_TOKEN,\n T_CUSTOMERS: $env.NOCODB_TABLE_CUSTOMERS,\n ALIST_URL: $env.ALIST_URL,\n ALIST_ADMIN_USERNAME: $env.ALIST_ADMIN_USERNAME,\n ALIST_ADMIN_PASSWORD: $env.ALIST_ADMIN_PASSWORD,\n ALIST_ADMIN_OTP: $env.ALIST_ADMIN_OTP || '',\n} };\n" }, "id": "load-env", "name": "Load Env", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 220, 0 ] }, { "parameters": { "method": "GET", "url": "={{ $('Load Env').item.json.NOCODB_URL }}/api/v2/tables/{{ $('Load Env').item.json.T_CUSTOMERS }}/records/{{ $json.customerId }}", "authentication": "none", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "xc-token", "value": "{{ $('Load Env').item.json.NOCODB_TOKEN }}" } ] }, "options": { "response": { "response": { "responseFormat": "json" } } } }, "id": "fetch-customer", "name": "Fetch Latest Customer Record", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 440, 0 ] }, { "parameters": { "jsCode": "const cust = $input.item.json;\nconst username = cust.username; const password = cust.password;\nif (!username) throw new Error('Customer missing username');\nconst newUserId = cust.alist_user_id ?? null;\nconst newRoleId = cust.alist_role_id ?? null;\nconst isCreate = !newUserId;\n// Break loop: only write back if semantic fields would actually change.\n// If the record already has alist_user_id + role_id + sync_status=synced,\n// this webhook was fired by our own write-back (echo) -> skip.\nconst skipWriteBack = (!isCreate) && (cust.sync_status === 'synced');\nreturn { json: {\n customerId: cust.Id ?? cust.id,\n username, password,\n displayName: cust.display_name ?? username,\n basePath: '/',\n alistUserId: newUserId,\n alistRoleId: newRoleId,\n isCreate,\n skipWriteBack,\n} };\n" }, "id": "prepare", "name": "Prepare Provision Payload", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 660, 0 ] }, { "parameters": { "method": "POST", "url": "={{ $('Load Env').item.json.ALIST_URL }}/api/auth/login", "authentication": "none", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ username: $('Load Env').item.json.ALIST_ADMIN_USERNAME, password: $('Load Env').item.json.ALIST_ADMIN_PASSWORD }) }}", "options": { "response": { "response": { "responseFormat": "json" } } } }, "id": "alist-login", "name": "AList Admin Login", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 880, 0 ] }, { "parameters": { "jsCode": "const resp = $input.item.json;\nconst token = resp?.data?.token;\nif (!token) throw new Error('AList admin login failed: ' + JSON.stringify(resp));\nreturn { json: { token } };\n" }, "id": "extract-token", "name": "Extract Admin Token", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1100, 0 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeName": "boolean", "ignoreCase": true }, "conditions": [ { "id": "cond-create", "leftValue": "={{ $('Prepare Provision Payload').item.json.isCreate }}", "rightValue": true, "operator": { "type": "boolean", "operation": "equals", "singleValue": true, "rightType": "boolean" } } ], "combinator": "or" }, "options": {} }, "id": "branch-create-update", "name": "Create vs Update", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ 1320, 0 ] }, { "parameters": { "method": "POST", "url": "={{ $('Load Env').item.json.ALIST_URL }}/api/admin/role/create", "authentication": "none", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Authorization", "value": "={{ $('Extract Admin Token').item.json.token }}" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ name: 'customer-' + $('Prepare Provision Payload').item.json.username, description: 'Auto role for ' + $('Prepare Provision Payload').item.json.username, default: false, permission_scopes: [] }) }}", "options": { "response": { "response": { "responseFormat": "json" } } } }, "id": "alist-create-role", "name": "AList Create Role", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1540, -240 ] }, { "parameters": { "method": "GET", "url": "={{ $('Load Env').item.json.ALIST_URL }}/api/admin/role/list?page=1&per_page=100", "authentication": "none", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Authorization", "value": "={{ $('Extract Admin Token').item.json.token }}" } ] }, "options": { "response": { "response": { "responseFormat": "json" } } } }, "id": "alist-list-roles", "name": "AList List Roles (find id)", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1760, -240 ] }, { "parameters": { "jsCode": "const roles = $input.item.json?.data?.content ?? [];\nconst target = 'customer-' + $('Prepare Provision Payload').item.json.username;\nconst r = roles.find(x => x.name === target);\nif (!r) throw new Error('Role not found: ' + target);\nreturn { json: { alistRoleId: r.id, roleName: target } };\n" }, "id": "find-role-id", "name": "Find New Role Id", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1980, -240 ] }, { "parameters": { "method": "POST", "url": "={{ $('Load Env').item.json.ALIST_URL }}/api/admin/user/create", "authentication": "none", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Authorization", "value": "={{ $('Extract Admin Token').item.json.token }}" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ username: $('Prepare Provision Payload').item.json.username, password: $('Prepare Provision Payload').item.json.password, base_path: $('Prepare Provision Payload').item.json.basePath, role: [$('Find New Role Id').item.json.alistRoleId], permission: 16384 }) }}", "options": { "response": { "response": { "responseFormat": "json" } } } }, "id": "alist-create-user", "name": "AList Create User", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 2200, -240 ] }, { "parameters": { "method": "GET", "url": "={{ $('Load Env').item.json.ALIST_URL }}/api/admin/user/list?page=1&per_page=100", "authentication": "none", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Authorization", "value": "={{ $('Extract Admin Token').item.json.token }}" } ] }, "options": { "response": { "response": { "responseFormat": "json" } } } }, "id": "alist-list-users", "name": "AList List Users (find id)", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 2420, -240 ] }, { "parameters": { "jsCode": "const users = $input.item.json?.data?.content ?? [];\nconst target = $('Prepare Provision Payload').item.json.username;\nconst u = users.find(x => x.username === target);\nif (!u) throw new Error('Created user not found: ' + target);\nreturn { json: { alistUserId: u.id, username: target } };\n" }, "id": "find-user-id", "name": "Find New User Id", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 2640, -240 ] }, { "parameters": { "method": "PATCH", "url": "={{ $('Load Env').item.json.NOCODB_URL }}/api/v2/tables/{{ $('Load Env').item.json.T_CUSTOMERS }}/records", "authentication": "none", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "xc-token", "value": "{{ $('Load Env').item.json.NOCODB_TOKEN }}" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify([{ Id: $('Prepare Provision Payload').item.json.customerId, alist_user_id: $('Find New User Id').item.json.alistUserId, alist_role_id: $('Find New Role Id').item.json.alistRoleId, sync_status: 'synced', last_sync_error: null }]) }}", "options": {} }, "id": "nocodb-writeback-create", "name": "Write Back (Created)", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 2860, -240 ] }, { "parameters": { "method": "POST", "url": "={{ $('Load Env').item.json.ALIST_URL }}/api/admin/user/update", "authentication": "none", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Authorization", "value": "={{ $('Extract Admin Token').item.json.token }}" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ id: $('Prepare Provision Payload').item.json.alistUserId, username: $('Prepare Provision Payload').item.json.username, password: $('Prepare Provision Payload').item.json.password, base_path: $('Prepare Provision Payload').item.json.basePath, role: $('Prepare Provision Payload').item.json.alistRoleId ? [$('Prepare Provision Payload').item.json.alistRoleId] : [], permission: 16384 }) }}", "options": { "response": { "response": { "responseFormat": "json" } } } }, "id": "alist-update-user", "name": "AList Update User", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1540, 0 ] }, { "parameters": { "method": "PATCH", "url": "={{ $('Load Env').item.json.NOCODB_URL }}/api/v2/tables/{{ $('Load Env').item.json.T_CUSTOMERS }}/records", "authentication": "none", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "xc-token", "value": "{{ $('Load Env').item.json.NOCODB_TOKEN }}" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify([{ Id: $('Prepare Provision Payload').item.json.customerId, sync_status: 'synced', last_sync_error: null }]) }}", "options": {} }, "id": "nocodb-writeback-update", "name": "Write Back (Updated)", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1760, 0 ] }, { "parameters": { "jsCode": "// Loop guard: echo webhook already synced -> stop here, no login, no write-back.\nreturn { json: { skipped: true, customerId: $('Prepare Provision Payload').item.json.customerId } };" }, "id": "skip-echo-end", "name": "Skip Echo (No-op)", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1540, -520 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeName": "boolean", "ignoreCase": true }, "conditions": [ { "id": "cond-skip", "leftValue": "={{ $('Prepare Provision Payload').item.json.skipWriteBack }}", "rightValue": true, "operator": { "type": "boolean", "operation": "equals", "singleValue": true, "rightType": "boolean" } } ], "combinator": "and" }, "options": {} }, "id": "if-skip", "name": "Skip if already synced", "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [ 1320, 0 ] } ], "connections": { "NocoDB: Customer Webhook": { "main": [ [ { "node": "Extract Customer Record Id", "type": "main", "index": 0 } ] ] }, "Extract Customer Record Id": { "main": [ [ { "node": "Load Env", "type": "main", "index": 0 } ] ] }, "Fetch Latest Customer Record": { "main": [ [ { "node": "Prepare Provision Payload", "type": "main", "index": 0 } ] ] }, "Prepare Provision Payload": { "main": [ [ { "node": "Skip if already synced", "type": "main", "index": 0 } ] ] }, "AList Admin Login": { "main": [ [ { "node": "Extract Admin Token", "type": "main", "index": 0 } ] ] }, "Extract Admin Token": { "main": [ [ { "node": "Create vs Update", "type": "main", "index": 0 } ] ] }, "Create vs Update": { "main": [ [ { "node": "AList Create Role", "type": "main", "index": 0 } ], [ { "node": "AList Update User", "type": "main", "index": 0 } ] ] }, "AList Create Role": { "main": [ [ { "node": "AList List Roles (find id)", "type": "main", "index": 0 } ] ] }, "AList List Roles (find id)": { "main": [ [ { "node": "Find New Role Id", "type": "main", "index": 0 } ] ] }, "Find New Role Id": { "main": [ [ { "node": "AList Create User", "type": "main", "index": 0 } ] ] }, "AList Create User": { "main": [ [ { "node": "AList List Users (find id)", "type": "main", "index": 0 } ] ] }, "AList List Users (find id)": { "main": [ [ { "node": "Find New User Id", "type": "main", "index": 0 } ] ] }, "Find New User Id": { "main": [ [ { "node": "Write Back (Created)", "type": "main", "index": 0 } ] ] }, "Write Back (Created)": { "main": [] }, "AList Update User": { "main": [ [ { "node": "Write Back (Updated)", "type": "main", "index": 0 } ] ] }, "Write Back (Updated)": { "main": [] }, "Load Env": { "main": [ [ { "node": "Fetch Latest Customer Record", "type": "main", "index": 0 } ] ] }, "Skip if already synced": { "main": [ [ { "node": "Skip Echo (No-op)", "type": "main", "index": 0 } ], [ { "node": "AList Admin Login", "type": "main", "index": 0 } ] ] } } }