{ "backed_up_at": "2026-07-10T12:58:15.925364", "workflow_count": 43, "workflows": { "Verona Networks PPPoE lease": { "id": "workflow_2ibweh1uRsDp5M289jEPqa", "description": null, "definition": { "steps": [ { "name": "Assign IP address to MAC address", "slug": "assign-ip-address-to-mac-address", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "allowSkipExecution": false, "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:mutation WorkflowAssignIpAddressToMacAddress($input: AssignIpAddressToMacAddressInput!) {\n assignIpAddressToMacAddress(input: $input) {\n id\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({\n input: {\n ipAddress: state.input.framedip,\n macAddress: state.input.callingstationid,\n accountId: state.steps['get-account-id-from-radius-username'].output?.accounts[0].accountId\n }\n})" } ], "nextSteps": [ { "slug": "run-update-preseem-on-pppoe-ip-change" } ] }, { "name": "Should assign IP address", "slug": "should-assign-ip-address", "type": "CONDITION", "nodeSlug": "condition", "condition": "({ secrets, state, utils, variables }) => {\n return state.input.bound === '1';\n}", "elseSteps": [ { "slug": "end" } ], "thenSteps": [ { "slug": "get-account-id-from-radius-username" } ] }, { "name": "Get Account ID from RADIUS Username", "slug": "get-account-id-from-radius-username", "type": "CUSTOM_WORKFLOW", "nodeSlug": "get-account-id-from-radius-username", "allowSkipExecution": false, "inputs": [ { "key": "username", "type": "STATE", "value": "input.username" } ], "nextSteps": [ { "slug": "assign-ip-address-to-mac-address" }, { "slug": "assign-ip-address-to-mac-address", "errorCodes": [ "USERNAME_NOT_FOUND" ] } ] }, { "allowSkipExecution": false, "type": "GAIIA_START_WORKFLOW_EXECUTION", "slug": "run-update-preseem-on-pppoe-ip-change", "name": "Run Update Preseem on PPPoE IP Change", "nodeSlug": "start-workflow-execution", "inputs": [ { "key": "workflowId", "value": "string:workflow_aEcWxEsubpuSebPwajqseP", "type": "LITERAL" }, { "key": "fireAndForget", "value": "boolean:true", "type": "LITERAL" } ], "nextSteps": [ { "slug": "end" } ] } ], "trigger": { "nextSteps": [ { "slug": "should-assign-ip-address" } ], "type": "WEBHOOK", "inputJsonSchema": { "type": "object", "required": [ "bound", "callingstationid", "framedip", "username" ], "properties": { "bound": { "type": "string" }, "framedip": { "type": "string" }, "username": { "type": "string" }, "callingstationid": { "type": "string" } } } } } }, "Get Config for FreeRADIUS": { "id": "workflow_1vgYQa82YuccZmRvzW5T3P", "description": null, "definition": { "trigger": { "nextSteps": [ { "slug": "get-free-radius-config" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => {\n const { instances } = state.steps[\"get-free-radius-config\"].output;\n const groupDetails = state.steps[\"get-attributes-and-fall-through\"].output;\n const groupDetailsMap = new Map();\n for (const detail of groupDetails) {\n groupDetailsMap.set(`${detail.instanceName}:${detail.groupName}`, detail);\n }\n const enrichedInstances = instances.map((instance) => ({\n ...instance,\n groups: instance.groups.map((group) => {\n const key = `${instance.name}:${group.groupName}`;\n const details = groupDetailsMap.get(key);\n return {\n ...group,\n attributes: details?.attributes || [],\n fallThrough: details?.fallThrough ?? false\n };\n })\n }));\n return { instances: enrichedInstances };\n }", "type": "JSON" }, "steps": [ { "name": "get-free-radius-config", "slug": "get-free-radius-config", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ utils }) => {\n const instancesString = await utils.integration.datastore.getItem(\"instances\");\n if (!instancesString) {\n return { instances: [] };\n }\n const instanceIds = instancesString.split(\",\").filter((id) => id.trim());\n const instances = [];\n for (const id of instanceIds) {\n const name = await utils.integration.datastore.getItem(`instances.${id}.name`);\n const [databaseHost, databasePort, databaseName, databaseUsername, radiusServerHost, coaPort, groups] = await Promise.all([\n utils.integration.datastore.getItem(`instances.${id}.databaseHost`),\n utils.integration.datastore.getItem(`instances.${id}.databasePort`),\n utils.integration.datastore.getItem(`instances.${id}.databaseName`),\n utils.integration.datastore.getItem(`instances.${id}.databaseUsername`),\n utils.integration.datastore.getItem(`instances.${id}.radiusServerHost`),\n utils.integration.datastore.getItem(`instances.${id}.coaPort`),\n utils.integration.datastore.getItem(`instances.${id}.groups`)\n ]);\n instances.push({\n id,\n name: name || \"\",\n databaseHost: databaseHost || \"\",\n databasePort: databasePort ? Number(databasePort) : 0,\n databaseName: databaseName || \"\",\n databaseUsername: databaseUsername || \"\",\n radiusServerHost: radiusServerHost || \"\",\n coaPort: coaPort ? Number(coaPort) : 0,\n groups: groups ? JSON.parse(groups) : []\n });\n }\n return { instances };\n }", "key": "function" } ], "nextSteps": [ { "slug": "get-attributes-and-fall-through" } ], "outputMapper": "({ output }) => output" }, { "name": "get-attributes-and-fall-through", "slug": "get-attributes-and-fall-through", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => {\n const { instances } = state.steps[\"get-free-radius-config\"].output;\n return instances.flatMap(\n (instance) => instance.groups.map((group) => ({\n instanceName: instance.name,\n groupName: group.groupName\n }))\n );\n }" } ], "initialSteps": [ { "slug": "query-group-details" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ state }) => state.steps[\"query-group-details\"].output" }, { "name": "query-group-details", "slug": "query-group-details", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.currentParallelLoopIterations[\"get-attributes-and-fall-through\"].instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { groupName } = state.currentParallelLoopIterations[\"get-attributes-and-fall-through\"];\n return [\n {\n query: `SELECT attribute, op, value FROM radgroupreply WHERE groupname = ?`,\n values: [groupName]\n }\n ];\n }", "key": "queries" } ], "nextSteps": [ { "slug": "get-attributes-and-fall-through-end" } ], "outputMapper": "({ state, output }) => {\n const loopItem = state.currentParallelLoopIterations[\"get-attributes-and-fall-through\"];\n const allResults = output.results[0] || [];\n const fallThroughRow = allResults.find((row) => row.attribute === \"Fall-Through\");\n const attributes = allResults.filter((row) => row.attribute !== \"Fall-Through\").map((row) => ({\n name: row.attribute,\n operator: row.op,\n value: row.value\n }));\n const fallThrough = !fallThroughRow || fallThroughRow.value === \"Yes\";\n return {\n groupName: loopItem.groupName,\n instanceName: loopItem.instanceName,\n attributes,\n fallThrough\n };\n }", "parentParallelLoopSlug": "get-attributes-and-fall-through" } ] } }, "Get Config for NetBox": { "id": "workflow_uncWf2LVGAEGAovxgYDRD8", "description": null, "definition": { "trigger": { "nextSteps": [ { "slug": "get-netbox-config" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => state.steps[\"get-netbox-config\"].output", "type": "JSON" }, "steps": [ { "name": "get-netbox-config", "slug": "get-netbox-config", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ utils }) => {\n const instancesString = await utils.integration.datastore.getItem(\"instances\");\n if (!instancesString) {\n return { instances: [] };\n }\n const instanceIds = instancesString.split(\",\").filter((name) => name.trim());\n const instances = [];\n for (const id of instanceIds) {\n const name = await utils.integration.datastore.getItem(`instances.${id}.name`);\n const baseUrl = await utils.integration.datastore.getItem(`instances.${id}.baseUrl`);\n const prefixSynchronizationString = await utils.integration.datastore.getItem(`instances.${id}.prefixSynchronization`);\n const networkSiteSynchronizationString = await utils.integration.datastore.getItem(`instances.${id}.networkSiteSynchronization`);\n let prefixSynchronization = { ipamRoles: [], isEnabled: false };\n if (prefixSynchronizationString) {\n try {\n prefixSynchronization = JSON.parse(prefixSynchronizationString);\n } catch {\n }\n }\n let networkSiteSynchronization = { customFieldMappings: [], isEnabled: false };\n if (networkSiteSynchronizationString) {\n try {\n networkSiteSynchronization = JSON.parse(networkSiteSynchronizationString);\n } catch {\n }\n }\n instances.push({\n id,\n baseUrl: baseUrl || \"\",\n name: name || \"\",\n prefixSynchronization,\n networkSiteSynchronization\n });\n }\n return { instances };\n }", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "Verona Networks DHCP lease": { "id": "workflow_rpEtJsMPzhNSYoM1H3jGch", "description": null, "definition": { "steps": [ { "name": "Find gaiia account by inventory item assignation", "slug": "find-gaiia-account-by-inventory-item-assignation", "outputMapper": "({ output, state }) => {\n const assignedInventoryItems = output.response.findInventoryItemsFromDeviceIdentifiers;\n if (assignedInventoryItems.length > 1) {\n workflow.fail(`Many inventory items with MAC '${state.input.mac}' or remote ID '${state.input.remoteId}' assigned to gaiia accounts.`, 'MANY_ASSIGNED_DEVICES');\n }\n return assignedInventoryItems[0]?.inventoryItem.assignation.assignee;\n }", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:query VeronaWorkflowFindInventoryItemsFromDeviceIdentifiersQuery($input: FindInventoryItemsFromDeviceIdentifiersQueryInput!) {\n findInventoryItemsFromDeviceIdentifiers(input: $input) {\n identifier\n inventoryItem {\n assignation {\n assignee {\n ... on Account {\n id\n }\n }\n }\n }\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => {\n const inventoryItemMacAddress = state.input.remoteId\n .split(':')\n .map((macAddressPart) => macAddressPart.length === 2 ? macAddressPart.toUpperCase() : `0${macAddressPart}`.toUpperCase())\n .join(':'); // a:0:3e:44:97:6 -> 0A:00:3E:44:97:06\n return {\n input: { identifiers: [inventoryItemMacAddress] }\n };\n }" } ], "nextSteps": [ { "slug": "assign-ip-address-to-mac-address-with-account" } ], "nodeSlug": "gaiia-internal-api-gateway-graphql-request" }, { "name": "Assign IP address to MAC address with account", "slug": "assign-ip-address-to-mac-address-with-account", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:mutation WorkflowAssignIpAddressToMacAddress($input: AssignIpAddressToMacAddressInput!) {\n assignIpAddressToMacAddress(input: $input) {\n id\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({\n input: {\n ipAddress: state.input.ip,\n macAddress: state.input.mac,\n accountId: state.steps['find-gaiia-account-by-inventory-item-assignation'].output?.id\n }\n })" } ], "nextSteps": [ { "slug": "end" } ], "nodeSlug": "gaiia-internal-api-gateway-graphql-request" }, { "name": "Assign IP address to MAC address without account", "slug": "assign-ip-address-to-mac-address-without-account", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:mutation WorkflowAssignIpAddressToMacAddress($input: AssignIpAddressToMacAddressInput!) {\n assignIpAddressToMacAddress(input: $input) {\n id\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({\n input: {\n ipAddress: state.input.ip,\n macAddress: state.input.mac\n }\n })" } ], "nextSteps": [ { "slug": "end" } ], "nodeSlug": "gaiia-internal-api-gateway-graphql-request" }, { "name": "Has remote ID", "slug": "has-remote-id", "condition": "({ secrets, state, utils, variables }) => {\n return state.input.remoteId;\n}", "elseSteps": [ { "slug": "assign-ip-address-to-mac-address-without-account" } ], "thenSteps": [ { "slug": "find-gaiia-account-by-inventory-item-assignation" } ], "type": "CONDITION", "nodeSlug": "condition" }, { "name": "Is IP address bound", "slug": "is-ip-address-bound", "condition": "({ secrets, state, utils, variables }) => {\n return state.input.bound === '1';\n}", "elseSteps": [ { "slug": "end" } ], "thenSteps": [ { "slug": "has-remote-id" } ], "type": "CONDITION", "nodeSlug": "condition" } ], "trigger": { "nextSteps": [ { "slug": "is-ip-address-bound" } ], "type": "WEBHOOK", "inputJsonSchema": { "type": "object", "required": [ "bound", "mac", "ip", "server", "remoteId" ], "properties": { "ip": { "type": "string" }, "mac": { "type": "string" }, "bound": { "type": "string" }, "server": { "type": "string" }, "remoteId": { "type": "string" } } } } } }, "NetBox HTTP Request": { "id": "workflow_w57zmPhtwUGon3AtuHSL3T", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "method": { "type": "string", "enum": [ "GET", "POST", "PUT", "DELETE", "PATCH" ], "description": "The HTTP method to use for the request." }, "path": { "type": "string", "description": "The URL path for the request." }, "body": { "type": [ "object", "array" ], "description": "Optional body of the request, typically used for POST and PUT requests." }, "params": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Optional query parameters for the request." }, "instanceName": { "type": "string" } }, "required": [ "method", "path" ] }, "nextSteps": [ { "slug": "fetch-datastore-key" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state }) => {\n if (\"output\" in state.steps[\"http-request\"]) {\n return state.steps[\"http-request\"].output.data;\n }\n return {};\n }", "type": "JSON" }, "steps": [ { "name": "fetch-datastore-key", "slug": "fetch-datastore-key", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => state.input", "key": "function" } ], "nextSteps": [ { "slug": "http-request" } ], "outputMapper": "async ({ state, utils }) => {\n const { instanceName } = state.input;\n let instanceId;\n if (!instanceName) {\n const instancesString = await utils.integration.datastore.getItem(\"instances\");\n if (!instancesString) {\n return utils.fail(\"No instances found in datastore\", \"NO_INSTANCES_FOUND\");\n }\n const instanceIds = instancesString.split(\",\").map((id) => id.trim()).filter(Boolean);\n if (instanceIds.length === 0) {\n utils.fail(\"No instance IDs found\", \"NO_INSTANCE_IDS_FOUND\");\n }\n [instanceId] = instanceIds;\n } else {\n const retrievedInstanceId = await utils.integration.datastore.getItem(`instances.${instanceName}.id`);\n if (!retrievedInstanceId) {\n return utils.fail(\"Invalid instance name provided\", \"INVALID_INSTANCE_NAME\");\n }\n instanceId = retrievedInstanceId;\n }\n const datastoreKey = `instances.${instanceId}`;\n const [baseURL, apiKey] = await Promise.all([\n utils.integration.datastore.getItem(`${datastoreKey}.baseURL`),\n utils.integration.datastore.getItem(`${datastoreKey}.apiKey`)\n ]);\n if (!baseURL)\n return utils.fail(\"Missing baseURL\", \"MISSING_BASE_URL\");\n if (!apiKey)\n return utils.fail(\"Missing API key\", \"MISSING_API_KEY\");\n return {\n baseURL,\n apiKey\n };\n }" }, { "name": "http-request", "slug": "http-request", "type": "HTTP_REST_REQUEST", "nodeSlug": "http-rest-request", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.input.body", "key": "body" }, { "type": "FUNCTION", "value": "async ({ state }) => ({\n Authorization: `Token ${state.steps[\"fetch-datastore-key\"].output.apiKey}`,\n \"Content-Type\": \"application/json\"\n })", "key": "headers" }, { "type": "FUNCTION", "value": "({ state }) => state.input.method", "key": "method" }, { "type": "FUNCTION", "value": "({ state }) => state.input.params", "key": "params" }, { "type": "FUNCTION", "value": "async ({ state }) => {\n const baseUrl = state.steps[\"fetch-datastore-key\"].output.baseURL;\n const baseUrlWithoutTrailingSlash = baseUrl.replace(/\\/$/, \"\");\n const pathWithoutLeadingSlash = state.input.path.replace(/^\\//, \"\");\n return `${baseUrlWithoutTrailingSlash}/${pathWithoutLeadingSlash}`;\n }", "key": "url" } ], "nextSteps": [ { "slug": "handle-http-failure", "errorCodes": [ "HTTP_REQUEST_FAILED" ] }, { "slug": "end" } ] }, { "name": "handle-http-failure", "slug": "handle-http-failure", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state, utils }) => {\n const IP_ADDRESS_NOT_FOUND_MESSAGE = \"Not found.\";\n const ADDRESS_ALREADY_RESERVED_MESSAGE = \"Duplicate IP address found\";\n const getNetboxErrorCode = (detail) => {\n if (detail.toLowerCase() === IP_ADDRESS_NOT_FOUND_MESSAGE.toLowerCase()) {\n return \"IP_ADDRESS_NOT_FOUND\";\n }\n return \"UNEXPECTED_ERROR\";\n };\n const failure = state.steps[\"http-request\"].error;\n const payload = failure.payload;\n if (payload?.detail) {\n return utils.fail(`Netbox returned the following error: ${payload.detail}`, getNetboxErrorCode(payload.detail), payload);\n }\n if (payload?.address && payload.address[0].toLowerCase().includes(ADDRESS_ALREADY_RESERVED_MESSAGE.toLowerCase())) {\n return utils.fail(\n `Netbox returned the following error: ${payload.address.map((message) => `'${message}'`).join(\", \")}`,\n \"ADDRESS_ALREADY_RESERVED\",\n payload\n );\n }\n return utils.fail(failure.message, failure.code, failure.payload);\n }", "key": "function" } ], "nextSteps": [ { "slug": "end" } ] } ] } }, "Add RADIUS Account": { "id": "workflow_mi1m5xC4nANZnbXyNgXKc4", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "accountId": { "type": "string", "description": "The account ID in gaiia" }, "username": { "type": "string", "description": "The RADIUS username", "maxLength": 64 }, "instanceName": { "type": "string", "description": "The FreeRADIUS instance name" }, "password": { "type": "string", "description": "The RADIUS user password", "maxLength": 253 }, "accountDetails": { "type": "object", "properties": { "framedIpAddress": { "type": "string", "description": "The Framed-IP-Address attribute" }, "framedRoute": { "type": "string", "description": "The Framed-Route attribute" } }, "additionalProperties": false } }, "required": [ "accountId", "username", "instanceName", "password" ], "additionalProperties": false }, "nextSteps": [ { "slug": "validate-input" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state, utils }) => {\n if (\"handle-error\" in state.steps) {\n utils.fail(state.steps[\"handle-error\"].output.message, state.steps[\"handle-error\"].output.code, state.steps[\"handle-error\"].output.payload);\n }\n return {\n success: true\n };\n }", "type": "JSON" }, "steps": [ { "name": "validate-input", "slug": "validate-input", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const { instanceName, username, password, accountDetails } = state.input;\n const instanceId = await utils.integration.datastore.getItem(`instances.${instanceName}.id`);\n if (!instanceId) {\n return utils.fail(\"Instance not found\", \"INSTANCE_NOT_FOUND\");\n }\n if (username.length > 64) {\n return utils.fail(\"Username is too long. Max length is 64\", \"USERNAME_TOO_LONG\");\n }\n const usernameRegex = /^[\\w +./:@`\\-]+$/;\n if (!usernameRegex.test(username)) {\n return utils.fail(\n \"The username contains invalid characters. Allowed characters are alphanumeric characters, plus: space, +, ., /, :, @, `, -, and _.\",\n \"USERNAME_INVALID_CHARACTERS\"\n );\n }\n if (password.length > 253) {\n return utils.fail(\"Password is too long. Max length is 253\", \"PASSWORD_TOO_LONG\");\n }\n const ipv4Regex = /^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d{1,2})\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d{1,2})$/;\n if (accountDetails?.framedIpAddress !== void 0 && accountDetails?.framedIpAddress !== null && accountDetails?.framedIpAddress !== \"\" && !ipv4Regex.test(accountDetails.framedIpAddress)) {\n return utils.fail(\"Framed-IP-Address must be a valid IPv4 address\", \"INVALID_FRAMED_IP_ADDRESS\");\n }\n if (accountDetails?.framedRoute !== void 0 && accountDetails?.framedRoute !== null && accountDetails?.framedRoute !== \"\") {\n const ipPart = accountDetails.framedRoute.split(\"/\")[0].trim();\n if (!ipv4Regex.test(ipPart)) {\n return utils.fail(\"Framed-Route must contain a valid IPv4 address\", \"INVALID_FRAMED_ROUTE\");\n }\n }\n return {\n instanceId,\n instanceName\n };\n }", "key": "function" } ], "nextSteps": [ { "slug": "check-username-and-attribute-availability" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ output }) => output" }, { "name": "check-username-and-attribute-availability", "slug": "check-username-and-attribute-availability", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { username, accountDetails } = state.input;\n const queries = [];\n queries.push({\n query: \"SELECT COUNT(*) as count FROM radcheck WHERE username = ? LIMIT 1\",\n values: [username]\n });\n if (accountDetails?.framedIpAddress) {\n queries.push({\n query: 'SELECT COUNT(*) as count FROM radreply WHERE attribute = \"Framed-IP-Address\" AND value = ?',\n values: [accountDetails.framedIpAddress]\n });\n }\n if (accountDetails?.framedRoute) {\n queries.push({\n query: 'SELECT COUNT(*) as count FROM radreply WHERE attribute = \"Framed-Route\" AND value = ?',\n values: [accountDetails.framedRoute]\n });\n }\n return queries;\n }", "key": "queries" } ], "nextSteps": [ { "slug": "get-account-details" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ state, output, utils }) => {\n const { accountDetails } = state.input;\n const [[usernameCount = { count: 0 }], ...restResults] = output.results;\n if (usernameCount.count > 0) {\n return utils.fail(\"Username already exists\", \"USERNAME_ALREADY_EXISTS\");\n }\n if (accountDetails?.framedIpAddress) {\n const [framedIpAddressCount] = restResults.shift();\n if (framedIpAddressCount.count > 0) {\n return utils.fail(\"Framed-IP-Address is not available\", \"FRAMED_IP_ADDRESS_NOT_AVAILABLE\");\n }\n }\n if (accountDetails?.framedRoute) {\n const [framedRouteCount] = restResults.shift();\n if (framedRouteCount.count > 0) {\n return utils.fail(\"Framed-Route is not available\", \"FRAMED_ROUTE_NOT_AVAILABLE\");\n }\n }\n return {\n success: true\n };\n }" }, { "name": "get-account-details", "slug": "get-account-details", "type": "GAIIA_API_GATEWAY_V1_GRAPHQL_REQUEST", "nodeSlug": "gaiia-public-api-gateway-graphql-request", "inputs": [ { "type": "FUNCTION", "value": "() => `\n query GetAccountAndProductSubscriptions($accountId: GlobalID!) {\n account(id: $accountId) {\n status {\n type\n }\n billingSubscriptions {\n nodes {\n productVersion {\n product {\n id\n }\n }\n unassignedAt\n }\n }\n }\n }\n `", "key": "query" }, { "type": "FUNCTION", "value": "({ state }) => ({\n accountId: state.input.accountId\n })", "key": "variables" } ], "nextSteps": [ { "slug": "create-radius-account" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "async ({ state, output, utils }) => {\n const { instanceId } = state.steps[\"validate-input\"].output;\n const response = output.response;\n const accountStatus = response.account?.status.type;\n if (!response.account) {\n return utils.fail(\"Account not found\", \"ACCOUNT_NOT_FOUND\");\n }\n const productIds = response.account.billingSubscriptions?.nodes.filter((node) => !node.unassignedAt).map((node) => node.productVersion.product.id) || [];\n const groupsData = await utils.integration.datastore.getItem(`instances.${instanceId}.groups`);\n let groups = [];\n if (groupsData) {\n const allGroups = JSON.parse(groupsData);\n const groupsToAssign = allGroups.filter((group) => {\n switch (group.mappingRule) {\n case \"ALL\":\n return true;\n case \"ACTIVE\":\n return accountStatus === \"ACTIVE\";\n case \"INACTIVE\":\n return accountStatus === \"INACTIVE\";\n case \"SUSPENDED\":\n return accountStatus === \"SUSPENDED\";\n case \"MATCH_PRODUCT\":\n const hasMatchingProduct = group.mappedProductIds.some((mappedProductId) => productIds.includes(mappedProductId));\n return hasMatchingProduct;\n }\n return false;\n });\n groups = groupsToAssign.map((group) => ({\n groupName: group.groupName,\n priority: group.priority\n }));\n }\n return {\n accountStatus: response.account.status.type,\n productIds,\n groups\n };\n }" }, { "name": "create-radius-account", "slug": "create-radius-account", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.input.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { username, password, accountDetails } = state.input;\n const { groups } = state.steps[\"get-account-details\"].output;\n const queries = [];\n queries.push({\n query: \"INSERT INTO radcheck (username, attribute, op, value) VALUES (?, ?, ?, ?)\",\n values: [username, \"Cleartext-Password\", \":=\", password]\n });\n if (accountDetails?.framedIpAddress) {\n queries.push({\n query: \"INSERT INTO radreply (username, attribute, op, value) VALUES (?, ?, ?, ?)\",\n values: [username, \"Framed-IP-Address\", \"=\", accountDetails.framedIpAddress]\n });\n }\n if (accountDetails?.framedRoute) {\n queries.push({\n query: \"INSERT INTO radreply (username, attribute, op, value) VALUES (?, ?, ?, ?)\",\n values: [username, \"Framed-Route\", \"+=\", accountDetails.framedRoute]\n });\n }\n if (groups.length > 0) {\n queries.push({\n query: `INSERT INTO radusergroup (username, groupname, priority) VALUES ${groups.map(() => \"(?, ?, ?)\").join(\", \")}`,\n values: groups.flatMap((group) => [username, group.groupName, String(group.priority)])\n });\n }\n return queries;\n }", "key": "queries" } ], "nextSteps": [ { "slug": "store-in-datastore" } ], "outputMapper": "() => ({\n success: true\n })" }, { "name": "store-in-datastore", "slug": "store-in-datastore", "type": "GAIIA_WRITE_TO_INTEGRATION_DATASTORE", "nodeSlug": "write-to-integration-datastore", "inputs": [ { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n const { accountId, username } = state.input;\n const { instanceId } = state.steps[\"validate-input\"].output;\n const existingData = await utils.integration.datastore.getItem(`radiusAccount.${accountId}`);\n const accounts = existingData ? JSON.parse(existingData) : [];\n const newAccount = {\n createdAt: (new Date()).toISOString(),\n instanceId,\n username\n };\n const updatedAccounts = [...accounts, newAccount];\n return [\n {\n key: `radiusAccount.${accountId}`,\n value: JSON.stringify(updatedAccounts)\n },\n {\n key: `radiusUsername.${instanceId}.${username}`,\n value: accountId\n }\n ];\n }", "key": "values" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "() => ({\n success: true\n })" }, { "name": "handle-error", "slug": "handle-error", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => {\n const steps = state.steps;\n return steps[\"check-username-and-attribute-availability\"]?.error || steps[\"get-account-details\"]?.error || steps[\"validate-input\"]?.error;\n }", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "Update RADIUS Account": { "id": "workflow_iNFhPzVMcuMd3WhXA1KfSE", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "instanceName": { "type": "string", "description": "The FreeRADIUS instance name" }, "accountId": { "type": "string", "description": "The account ID in gaiia" }, "oldUsername": { "type": "string", "description": "The current RADIUS username" }, "username": { "type": "string", "description": "The new RADIUS username", "maxLength": 64 }, "password": { "type": "string", "description": "The new password (if changing)", "maxLength": 253 }, "framedIpAddress": { "type": "string", "description": "The Framed-IP-Address attribute" }, "framedRoute": { "type": "string", "description": "The Framed-Route attribute (without gateway)" } }, "required": [ "instanceName", "accountId", "oldUsername", "username" ], "additionalProperties": false }, "nextSteps": [ { "slug": "get-instance-coa-config" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state, utils }) => {\n if (\"handle-error\" in state.steps) {\n const error = state.steps[\"handle-error\"];\n utils.fail(error.output.message, error.output.code, error.output.payload);\n }\n return {\n success: true\n };\n }", "type": "JSON" }, "steps": [ { "name": "get-instance-coa-config", "slug": "get-instance-coa-config", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const { instanceName, username, password, framedIpAddress, framedRoute } = state.input;\n const instanceId = await utils.integration.datastore.getItem(`instances.${instanceName}.id`);\n if (!instanceId) {\n return utils.fail(\"Instance not found\", \"INSTANCE_NOT_FOUND\");\n }\n const [radiusServerHost, coaPort, coaSecret] = await Promise.all([\n utils.integration.datastore.getItem(`instances.${instanceId}.radiusServerHost`),\n utils.integration.datastore.getItem(`instances.${instanceId}.coaPort`),\n utils.integration.datastore.getItem(`instances.${instanceId}.coaSecret`)\n ]);\n if (username.length > 64) {\n return utils.fail(\"Username is too long. Max length is 64\", \"USERNAME_TOO_LONG\");\n }\n const usernameRegex = /^[\\w +./:@`\\-]+$/;\n if (!usernameRegex.test(username)) {\n return utils.fail(\n \"The username contains invalid characters. Allowed characters are alphanumeric characters, plus: space, +, ., /, :, @, `, -, and _.\",\n \"USERNAME_INVALID_CHARACTERS\"\n );\n }\n if (password !== void 0 && password !== null && password.length > 253) {\n return utils.fail(\"Password is too long. Max length is 253\", \"PASSWORD_TOO_LONG\");\n }\n const ipv4Regex = /^(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d{1,2})\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d{1,2})$/;\n if (framedIpAddress !== void 0 && framedIpAddress !== null && framedIpAddress !== \"\" && !ipv4Regex.test(framedIpAddress)) {\n return utils.fail(\"Framed-IP-Address must be a valid IPv4 address\", \"INVALID_FRAMED_IP_ADDRESS\");\n }\n if (framedRoute !== void 0 && framedRoute !== null && framedRoute !== \"\") {\n const ipPart = framedRoute.split(\"/\")[0].trim();\n if (!ipv4Regex.test(ipPart)) {\n return utils.fail(\"Framed-Route must contain a valid IPv4 address\", \"INVALID_FRAMED_ROUTE\");\n }\n }\n return {\n instanceId,\n instanceName,\n radiusServerHost: radiusServerHost || \"\",\n coaPort: coaPort ? Number(coaPort) : 0,\n coaSecret: coaSecret || \"\"\n };\n }", "key": "function" } ], "nextSteps": [ { "slug": "fetch-account-state-and-validate-transaction" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ output }) => output" }, { "name": "fetch-account-state-and-validate-transaction", "slug": "fetch-account-state-and-validate-transaction", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-instance-coa-config\"].output.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const queries = [\n {\n query: `SELECT rc.attribute, rc.value FROM radcheck rc WHERE rc.username = ? AND rc.attribute LIKE ? AND rc.value IS NOT NULL;`,\n values: [state.input.oldUsername, \"%-Password\"]\n },\n {\n query: `SELECT rr.attribute, rr.value FROM radreply rr WHERE rr.username = ? AND rr.value IS NOT NULL;`,\n values: [state.input.oldUsername]\n }\n ];\n if (state.input.oldUsername !== state.input.username) {\n queries.push({\n query: \"SELECT COUNT(*) as count FROM radcheck WHERE username = ? LIMIT 1\",\n values: [state.input.username]\n });\n }\n if (state.input.framedIpAddress) {\n queries.push({\n query: 'SELECT COUNT(*) as count FROM radreply WHERE attribute = \"Framed-IP-Address\" AND value = ? AND username <> ?',\n values: [state.input.framedIpAddress, state.input.oldUsername]\n });\n }\n if (state.input.framedRoute) {\n queries.push({\n query: 'SELECT COUNT(*) as count FROM radreply WHERE attribute = \"Framed-Route\" AND value = ? AND username <> ?',\n values: [`${state.input.framedRoute} 0.0.0.0 1`, state.input.oldUsername]\n });\n }\n return queries;\n }", "key": "queries" } ], "nextSteps": [ { "slug": "update-account-details" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ state, output, utils }) => {\n const { oldUsername, username, framedIpAddress, framedRoute } = state.input;\n const [oldPasswordResults, oldUsernameRadreplyResults, ...restResults] = output.results;\n const oldPasswordRow = oldPasswordResults?.find((r) => r.attribute && r.attribute.toLowerCase() === \"cleartext-password\");\n const oldPassword = oldPasswordRow?.value || null;\n const oldFramedIpAddressRow = oldUsernameRadreplyResults?.find((r) => r.attribute === \"Framed-IP-Address\");\n const oldFramedRouteRow = oldUsernameRadreplyResults?.find((r) => r.attribute === \"Framed-Route\");\n const oldFramedIpAddress = oldFramedIpAddressRow?.value || null;\n const oldFramedRoute = oldFramedRouteRow?.value || null;\n if (oldUsername !== username) {\n const [newUsernameExists] = restResults.shift();\n if (newUsernameExists.count > 0) {\n return utils.fail(\"Username already exists\", \"USERNAME_ALREADY_EXISTS\");\n }\n }\n if (framedIpAddress) {\n const [framedIpAddressExists] = restResults.shift();\n if (framedIpAddressExists.count > 0) {\n return utils.fail(\"Framed-IP-Address is not available\", \"FRAMED_IP_ADDRESS_NOT_AVAILABLE\");\n }\n }\n if (framedRoute) {\n const [framedRouteExists] = restResults.shift();\n if (framedRouteExists.count > 0) {\n return utils.fail(\"Framed-Route is not available\", \"FRAMED_ROUTE_NOT_AVAILABLE\");\n }\n }\n return {\n password: oldPassword,\n framedIpAddress: oldFramedIpAddress,\n framedRoute: oldFramedRoute\n };\n }" }, { "name": "update-account-details", "slug": "update-account-details", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-instance-coa-config\"].output.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { oldUsername, username, password, framedIpAddress, framedRoute } = state.input;\n const currentPassword = state.steps[\"fetch-account-state-and-validate-transaction\"].output.password;\n const currentFramedIpAddress = state.steps[\"fetch-account-state-and-validate-transaction\"].output.framedIpAddress;\n const currentFramedRoute = state.steps[\"fetch-account-state-and-validate-transaction\"].output.framedRoute;\n const currentFramedRouteWithGateway = currentFramedRoute ? `${currentFramedRoute} 0.0.0.0 1` : null;\n const queries = [];\n if (oldUsername !== username) {\n queries.push({\n query: `UPDATE radcheck rc\n LEFT JOIN radreply rr ON rc.username = rr.username\n LEFT JOIN radusergroup rug ON rc.username = rug.username\n SET\n rc.username = ?,\n rr.username = ?,\n rug.username = ?\n WHERE\n rc.username = ?\n AND rc.attribute LIKE ?`,\n values: [username, username, username, oldUsername, \"%-Password\"]\n });\n }\n if (password && password !== currentPassword) {\n queries.push({\n query: `UPDATE radcheck rc\n SET\n rc.value = ?,\n rc.attribute = ?\n WHERE\n rc.username = ?\n AND rc.attribute LIKE ?`,\n values: [password, \"Cleartext-Password\", username, \"%-Password\"]\n });\n }\n if (framedIpAddress !== void 0 && framedIpAddress !== currentFramedIpAddress) {\n if (currentFramedIpAddress && !framedIpAddress) {\n queries.push({\n query: `DELETE FROM radreply\n WHERE\n username = ?\n AND attribute = ?\n LIMIT 1`,\n values: [username, \"Framed-IP-Address\"]\n });\n } else if (framedIpAddress) {\n const hasPreviousValue = currentFramedIpAddress !== null;\n if (hasPreviousValue) {\n queries.push({\n query: `UPDATE radreply\n SET value = ?\n WHERE\n username = ?\n AND attribute = ?\n LIMIT 1`,\n values: [framedIpAddress, username, \"Framed-IP-Address\"]\n });\n } else {\n queries.push({\n query: `INSERT INTO radreply(username, attribute, op, value)\n VALUES (?, ?, ?, ?)`,\n values: [username, \"Framed-IP-Address\", \"=\", framedIpAddress]\n });\n }\n }\n }\n if (framedRoute !== void 0 && framedRoute !== currentFramedRoute) {\n const framedRouteWithGateway = framedRoute ? `${framedRoute} 0.0.0.0 1` : null;\n if (currentFramedRouteWithGateway && !framedRouteWithGateway) {\n queries.push({\n query: `DELETE FROM radreply\n WHERE\n username = ?\n AND attribute = ?\n LIMIT 1`,\n values: [username, \"Framed-Route\"]\n });\n } else if (framedRouteWithGateway) {\n const hasPreviousValue = currentFramedRouteWithGateway !== null;\n if (hasPreviousValue) {\n queries.push({\n query: `UPDATE radreply\n SET value = ?\n WHERE\n username = ?\n AND attribute = ?\n LIMIT 1`,\n values: [framedRouteWithGateway, username, \"Framed-Route\"]\n });\n } else {\n queries.push({\n query: `INSERT INTO radreply(username, attribute, op, value)\n VALUES (?, ?, ?, ?)`,\n values: [username, \"Framed-Route\", \"+=\", framedRouteWithGateway]\n });\n }\n }\n }\n queries.push({\n query: `\n SELECT\n acctsessionid as accountSessionId,\n nasipaddress as nasIpAddress,\n callingstationid as callingStationId\n FROM\n radacct\n WHERE\n username = ? AND acctstoptime IS NULL\n LIMIT 1;\n `,\n values: [oldUsername, oldUsername, oldUsername, oldUsername]\n });\n return queries;\n }", "key": "queries" } ], "nextSteps": [ { "slug": "check-active-session" } ], "outputMapper": "({ output }) => {\n const sessionResults = output.results[output.results.length - 1];\n const [session] = sessionResults || [];\n const oldUsername = session?.oldUsername || null;\n const attributes = {\n \"User-Name\": oldUsername || \"\",\n ...session?.nasIpAddress ? { \"NAS-IP-Address\": session.nasIpAddress } : {},\n ...session?.accountSessionId ? { \"Acct-Session-Id\": session.accountSessionId } : {},\n ...session?.callingStationId ? { \"Calling-Station-Id\": session.callingStationId } : {}\n };\n return {\n hasActiveSession: !!session,\n attributes,\n oldUsername\n };\n }" }, { "name": "check-active-session", "slug": "check-active-session", "nodeSlug": "condition", "type": "CONDITION", "condition": "async ({ state }) => state.steps[\"update-account-details\"].output.hasActiveSession", "thenSteps": [ { "slug": "send-coa-packet" } ], "elseSteps": [ { "slug": "update-datastore" } ] }, { "name": "send-coa-packet", "slug": "send-coa-packet", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-send-coa-packet", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.steps[\"update-account-details\"].output.attributes", "key": "attributes" }, { "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-instance-coa-config\"].output.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "update-datastore" } ], "outputMapper": "({ output }) => ({ success: output.success })" }, { "name": "update-datastore", "slug": "update-datastore", "type": "GAIIA_WRITE_TO_INTEGRATION_DATASTORE", "nodeSlug": "write-to-integration-datastore", "inputs": [ { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n const { accountId, oldUsername, username } = state.input;\n const { instanceId } = state.steps[\"get-instance-coa-config\"].output;\n const existingData = await utils.integration.datastore.getItem(`radiusAccount.${accountId}`);\n if (!existingData) {\n return utils.fail(\"Account not found in datastore\", \"ACCOUNT_NOT_FOUND\");\n }\n const accounts = JSON.parse(existingData);\n const account = accounts.find((account2) => account2.instanceId === instanceId && account2.username === oldUsername);\n if (!account) {\n return utils.fail(\"Account not found in datastore\", \"ACCOUNT_NOT_FOUND\");\n }\n const updatedAccount = {\n ...account,\n username\n };\n const updatedAccounts = accounts.map((a) => a.instanceId === instanceId && a.username === oldUsername ? updatedAccount : a);\n const updates = [\n {\n key: `radiusAccount.${accountId}`,\n value: JSON.stringify(updatedAccounts)\n }\n ];\n if (oldUsername !== username) {\n updates.push(\n {\n key: `radiusUsername.${instanceId}.${oldUsername}`,\n value: \"\"\n // Empty string to delete the reverse mapping\n },\n {\n key: `radiusUsername.${instanceId}.${username}`,\n value: accountId\n }\n );\n }\n return updates;\n }", "key": "values" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "() => ({\n success: true\n })" }, { "name": "handle-error", "slug": "handle-error", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => {\n const steps = state.steps;\n return steps[\"fetch-account-state-and-validate-transaction\"]?.error || steps[\"get-instance-coa-config\"]?.error;\n }", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "Verona Networks activation": { "id": "workflow_dK9EZgPKnUMQa6RmnqtFny", "description": null, "definition": { "steps": [ { "name": "Find gaiia account", "nextSteps": [ { "slug": "find-account-cpes" } ], "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "slug": "find-gaiia-account", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:query VeronaActivationWorkflowAccountQuery($id: ID!) {\n account(id: $id) {\n gaiiaId\n primaryContact {\n firstName\n lastName\n }\n activeIpAddresses {\n ipAddress\n macAddress\n }\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({ id: state.accountId })" } ], "outputMapper": "({ output }) => {\n const { account } = output.response;\n if (account.activeIpAddresses.length === 0) {\n workflow.fail(`No active IP address associated to this account.`, 'NO_ACTIVE_IP_ADDRESS');\n }\n return account;\n}" }, { "name": "Find account CPEs", "nextSteps": [ { "slug": "find-account-internet-plans" } ], "nodeSlug": "find-account-cpes", "slug": "find-account-cpes", "type": "GAIIA_FIND_ACCOUNT_CPES", "inputs": [ { "key": "accountId", "type": "STATE", "value": "accountId" } ] }, { "name": "Find account Internet plans", "nextSteps": [ { "slug": "preseem-create-account" } ], "nodeSlug": "find-account-internet-plans", "slug": "find-account-internet-plans", "type": "GAIIA_FIND_ACCOUNT_INTERNET_PLANS", "inputs": [ { "key": "accountId", "type": "STATE", "value": "accountId" } ] }, { "name": "Preseem create account", "nextSteps": [ { "slug": "preseem-create-service" } ], "nodeSlug": "preseem-rest-request", "slug": "preseem-create-account", "type": "PRESEEM_REST_REQUEST", "inputs": [ { "key": "method", "type": "LITERAL", "value": "string:PUT" }, { "key": "path", "type": "FUNCTION", "value": "({ state }) => `/accounts/${state.steps['find-gaiia-account'].output.gaiiaId}`" }, { "key": "body", "type": "FUNCTION", "value": "({ state }) => {\n const { gaiiaId, primaryContact: { firstName, lastName } } = state.steps['find-gaiia-account'].output;\n return {\n id: String(gaiiaId),\n name: `${firstName} ${lastName}`\n };\n }" } ] }, { "name": "Preseem create service", "nextSteps": [ { "slug": "end" } ], "nodeSlug": "preseem-rest-request", "slug": "preseem-create-service", "type": "PRESEEM_REST_REQUEST", "inputs": [ { "key": "method", "type": "LITERAL", "value": "string:PUT" }, { "key": "path", "type": "FUNCTION", "value": "({ state }) => `/services/${state.steps['find-gaiia-account'].output.gaiiaId}`" }, { "key": "body", "type": "FUNCTION", "value": "({ state }) => {\n const { gaiiaId, activeIpAddresses } = state.steps['find-gaiia-account'].output;\n const [{ slug }] = state.steps['find-account-internet-plans'].output.internetPlans;\n const cpes = state.steps['find-account-cpes'].output.accountCpe;\n const macAddresses = [\n ...new Set([...activeIpAddresses.map(({ macAddress }) => macAddress), ...cpes.map(({ macAddress }) => macAddress)])\n ];\n return {\n id: String(gaiiaId),\n attachments: macAddresses.map((macAddress) => ({ cpe_mac: macAddress.toLowerCase() })),\n account: String(gaiiaId),\n package: slug\n };\n }" } ] } ], "trigger": { "nextSteps": [ { "slug": "find-gaiia-account" } ], "type": "MANUAL", "objectType": "ACCOUNT" } } }, "Delete RADIUS Account": { "id": "workflow_3JJjHTGECV3bpfXwMEMkqc", "description": null, "definition": { "trigger": { "inputJsonSchema": { "type": "object", "properties": { "username": { "type": "string", "description": "The RADIUS username" }, "instanceName": { "type": "string", "description": "The FreeRADIUS instance name" }, "accountId": { "type": "string", "description": "The account ID in gaiia" } }, "required": [ "username", "instanceName", "accountId" ], "additionalProperties": false }, "nextSteps": [ { "slug": "get-instance-coa-config" } ], "type": "MANUAL" }, "outputMapper": { "function": "({ state, utils }) => {\n if (\"handle-error\" in state.steps) {\n const error = state.steps[\"handle-error\"];\n utils.fail(error.output.message, error.output.code, error.output.payload);\n }\n return {\n success: true\n };\n }", "type": "JSON" }, "steps": [ { "name": "get-instance-coa-config", "slug": "get-instance-coa-config", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:async ({ state, utils }) => {\n const { instanceName } = state.input;\n const instanceId = await utils.integration.datastore.getItem(`instances.${instanceName}.id`);\n if (!instanceId) {\n return utils.fail(\"Instance not found\", \"INSTANCE_NOT_FOUND\");\n }\n const [radiusServerHost, coaPort, coaSecret] = await Promise.all([\n utils.integration.datastore.getItem(`instances.${instanceId}.radiusServerHost`),\n utils.integration.datastore.getItem(`instances.${instanceId}.coaPort`),\n utils.integration.datastore.getItem(`instances.${instanceId}.coaSecret`)\n ]);\n return {\n instanceId,\n instanceName,\n radiusServerHost: radiusServerHost || \"\",\n coaPort: coaPort ? Number(coaPort) : 0,\n coaSecret: coaSecret || \"\"\n };\n }", "key": "function" } ], "nextSteps": [ { "slug": "get-account-details" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ output }) => output" }, { "name": "get-account-details", "slug": "get-account-details", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-instance-coa-config\"].output.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { username } = state.input;\n return [\n {\n query: `\n SELECT\n rc.username,\n rc.attribute,\n rc.value as password\n FROM\n radcheck rc\n WHERE\n rc.username = ?\n AND rc.attribute LIKE '%-Password'\n AND rc.value IS NOT NULL\n LIMIT 1;\n `,\n values: [username]\n },\n {\n query: `\n SELECT\n rug.groupname as groupName,\n rug.priority\n FROM\n radusergroup rug\n WHERE\n rug.username = ?;\n `,\n values: [username]\n },\n {\n query: `\n SELECT\n rr.attribute,\n rr.value\n FROM\n radreply rr\n WHERE\n rr.username = ?\n AND rr.attribute IN ('Framed-IP-Address', 'Framed-Route')\n AND rr.value IS NOT NULL;\n `,\n values: [username]\n }\n ];\n }", "key": "queries" } ], "nextSteps": [ { "slug": "delete-radius-account" }, { "slug": "handle-error", "errorCodes": [ "*" ] } ], "outputMapper": "({ output, utils }) => {\n const accountDetailsResults = output.results[0];\n const passwordRow = accountDetailsResults.find((row) => row.attribute?.toLowerCase().includes(\"password\"));\n if (!passwordRow) {\n utils.fail(\"User not found in RADIUS database\", \"USERNAME_NOT_FOUND\");\n }\n const userGroupsResults = output.results[1];\n const groups = userGroupsResults.map((row) => ({\n groupName: row.groupName,\n priority: row.priority\n }));\n const radreplyResults = output.results[2]?.results || [];\n const framedIpAddress = radreplyResults.find((row) => row.attribute === \"Framed-IP-Address\")?.value;\n const framedRoute = radreplyResults.find((row) => row.attribute === \"Framed-Route\")?.value;\n return {\n groups,\n framedIpAddress: framedIpAddress || \"\",\n framedRoute: framedRoute || \"\",\n hasFramedIpAddress: !!framedIpAddress,\n hasFramedRoute: !!framedRoute\n };\n }" }, { "name": "delete-radius-account", "slug": "delete-radius-account", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-mysql-transaction", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-instance-coa-config\"].output.instanceName", "key": "instanceName" }, { "type": "FUNCTION", "value": "({ state }) => {\n const { username } = state.input;\n const { groups } = state.steps[\"get-account-details\"].output;\n const queries = [];\n queries.push({\n query: `\n SELECT\n acctsessionid as accountSessionId,\n nasipaddress as nasIpAddress,\n callingstationid as callingStationId\n FROM\n radacct\n WHERE\n username = ? AND acctstoptime IS NULL;\n `,\n values: [username]\n });\n if (groups.length > 0) {\n const groupPlaceholders = groups.map(() => \"?\").join(\", \");\n queries.push({\n query: `DELETE FROM radusergroup WHERE username = ? AND groupname IN (${groupPlaceholders})`,\n values: [username, ...groups.map((g) => g.groupName)]\n });\n }\n queries.push(\n // Delete only Framed-IP-Address and Framed-Route from radreply for this username\n {\n query: `DELETE FROM radreply WHERE username = ? AND attribute IN ('Framed-IP-Address', 'Framed-Route')`,\n values: [username]\n },\n // Delete from radcheck (LIMIT 1, matches radiusDeleteRadcheckCredentials)\n {\n query: \"DELETE FROM radcheck WHERE username = ? LIMIT 1\",\n values: [username]\n }\n );\n return queries;\n }", "key": "queries" } ], "nextSteps": [ { "slug": "check-active-session" } ], "outputMapper": "({ output, state }) => {\n const [sessions] = output.results;\n const hasActiveSession2 = sessions && sessions.length > 0;\n const sessionsAttributes = (sessions || []).map((session) => ({\n \"User-Name\": state.input.username,\n \"NAS-IP-Address\": session.nasIpAddress,\n \"Acct-Session-Id\": session.accountSessionId,\n \"Calling-Station-Id\": session.callingStationId\n }));\n return {\n hasActiveSession: hasActiveSession2,\n sessions: sessions || [],\n sessionsAttributes\n };\n }" }, { "name": "check-active-session", "slug": "check-active-session", "nodeSlug": "condition", "type": "CONDITION", "condition": "async ({ state }) => state.steps[\"delete-radius-account\"].output.hasActiveSession", "thenSteps": [ { "slug": "send-coa-packets" } ], "elseSteps": [ { "slug": "delete-from-datastore" } ] }, { "name": "send-coa-packets", "slug": "send-coa-packets", "nodeSlug": "parallel-loop", "type": "PARALLEL_LOOP", "inputs": [ { "key": "iterable", "type": "FUNCTION", "value": "({ state }) => {\n const { sessionsAttributes } = state.steps[\"delete-radius-account\"].output;\n return sessionsAttributes;\n }" } ], "initialSteps": [ { "slug": "send-coa-disconnect" } ], "nextSteps": [ { "slug": "delete-from-datastore" } ], "outputMapper": "() => ({ success: true })" }, { "name": "send-coa-disconnect", "slug": "send-coa-disconnect", "type": "CUSTOM_WORKFLOW", "nodeSlug": "free-radius-send-coa-packet", "inputs": [ { "type": "FUNCTION", "value": "({ state }) => state.currentParallelLoopIterations[\"send-coa-packets\"]", "key": "attributes" }, { "type": "FUNCTION", "value": "({ state }) => state.steps[\"get-instance-coa-config\"].output.instanceName", "key": "instanceName" } ], "nextSteps": [ { "slug": "send-coa-packets-end" } ], "outputMapper": "({ output }) => ({ success: output.success })", "parentParallelLoopSlug": "send-coa-packets" }, { "name": "delete-from-datastore", "slug": "delete-from-datastore", "type": "GAIIA_WRITE_TO_INTEGRATION_DATASTORE", "nodeSlug": "write-to-integration-datastore", "inputs": [ { "type": "FUNCTION", "value": "async ({ state, utils }) => {\n const { accountId, username } = state.input;\n const { instanceId } = state.steps[\"get-instance-coa-config\"].output;\n const existingData = await utils.integration.datastore.getItem(`radiusAccount.${accountId}`);\n if (!existingData) {\n return utils.fail(\"Account not found in datastore\", \"ACCOUNT_NOT_FOUND\");\n }\n const accounts = JSON.parse(existingData);\n const updatedAccounts = accounts.filter((account) => !(account.instanceId === instanceId && account.username === username));\n return [\n {\n key: `radiusAccount.${accountId}`,\n value: JSON.stringify(updatedAccounts)\n },\n {\n key: `radiusUsername.${instanceId}.${username}`,\n value: \"\"\n // Empty string to delete the reverse mapping\n }\n ];\n }", "key": "values" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "() => ({\n success: true\n })" }, { "name": "handle-error", "slug": "handle-error", "type": "TYPESCRIPT_CODE", "nodeSlug": "execute-typescript-code", "inputs": [ { "type": "LITERAL", "value": "function:({ state }) => {\n const steps = state.steps;\n return steps[\"delete-from-datastore\"]?.error || steps[\"check-active-session\"]?.error || steps[\"send-coa-disconnect\"]?.error;\n }", "key": "function" } ], "nextSteps": [ { "slug": "end" } ], "outputMapper": "({ output }) => output" } ] } }, "Verona Networks cancellation": { "id": "workflow_t9iyLCe6xTJUrJALiBjpsL", "description": null, "definition": { "steps": [ { "name": "Preseem delete account", "nextSteps": [ { "slug": "create-cancellation-ticket" } ], "nodeSlug": "preseem-rest-request", "slug": "preseem-delete-account", "type": "PRESEEM_REST_REQUEST", "inputs": [ { "key": "method", "type": "LITERAL", "value": "string:DELETE" }, { "key": "path", "type": "FUNCTION", "value": "({ state }) => `/accounts/${state.steps['find-gaiia-account'].output.response.account.gaiiaId}`" } ] }, { "name": "Create cancellation ticket", "nextSteps": [ { "slug": "end" } ], "nodeSlug": "gaiia-internal-api-gateway-graphql-request", "slug": "create-cancellation-ticket", "type": "GAIIA_API_GATEWAY_V0_GRAPHQL_REQUEST", "inputs": [ { "key": "query", "type": "LITERAL", "value": "string:mutation WorkflowCreateTicketMutation($input: CreateTicketInput!) {\n createTicket(input: $input) {\n id\n }\n}" }, { "key": "variables", "type": "FUNCTION", "value": "({ state }) => ({\n input: {\n title: `Account ${state.steps['find-gaiia-account'].output.response.account.gaiiaId} | Service Cancellation - Equipment Recovery`,\n typeId: 'e6d7b178-11f1-4ea6-bb20-a5bab438b64f', // Update for e6d7b178-11f1-4ea6-bb20-a5bab438b64f in prod\n priority: 'LOW',\n description: `