diff --git a/packages/core/bin/generate-ui-types b/packages/core/bin/generate-ui-types
index 988462d604..8cecb6b054 100755
--- a/packages/core/bin/generate-ui-types
+++ b/packages/core/bin/generate-ui-types
@@ -33,8 +33,11 @@ function findReferencedMethods(obj, refs = {}, latestName = '') {
const knownCredentials = loader.known.credentials;
const credentialTypes = Object.values(loader.credentialTypes).map((data) => {
const credentialType = data.type;
- if (knownCredentials[credentialType.name].supportedNodes?.length > 0) {
- delete credentialType.httpRequestNode;
+ if (
+ knownCredentials[credentialType.name].supportedNodes?.length > 0 &&
+ credentialType.httpRequestNode
+ ) {
+ credentialType.httpRequestNode.hidden = true;
}
return credentialType;
});
diff --git a/packages/editor-ui/src/stores/credentials.store.ts b/packages/editor-ui/src/stores/credentials.store.ts
index e5be582ea2..f2e209370a 100644
--- a/packages/editor-ui/src/stores/credentials.store.ts
+++ b/packages/editor-ui/src/stores/credentials.store.ts
@@ -206,7 +206,9 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
});
const httpOnlyCredentialTypes = computed(() => {
- return allCredentialTypes.value.filter((credentialType) => credentialType.httpRequestNode);
+ return allCredentialTypes.value.filter(
+ (credentialType) => credentialType.httpRequestNode && !credentialType.httpRequestNode.hidden,
+ );
});
// #endregion
diff --git a/packages/nodes-base/credentials/OktaApi.credentials.ts b/packages/nodes-base/credentials/OktaApi.credentials.ts
index c5b925c071..2e690bfb77 100644
--- a/packages/nodes-base/credentials/OktaApi.credentials.ts
+++ b/packages/nodes-base/credentials/OktaApi.credentials.ts
@@ -30,12 +30,13 @@ export class OktaApi implements ICredentialType {
placeholder: 'https://dev-123456.okta.com',
},
{
- displayName: 'SSWS Access Token',
+ displayName: 'Access Token',
name: 'accessToken',
type: 'string',
typeOptions: { password: true },
required: true,
default: '',
+ description: 'Secure Session Web Service Access Token',
},
];
diff --git a/packages/nodes-base/nodes/Okta/Okta.dark.svg b/packages/nodes-base/nodes/Okta/Okta.dark.svg
new file mode 100644
index 0000000000..b6f1fd7e2b
--- /dev/null
+++ b/packages/nodes-base/nodes/Okta/Okta.dark.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/nodes-base/nodes/Okta/Okta.node.ts b/packages/nodes-base/nodes/Okta/Okta.node.ts
new file mode 100644
index 0000000000..e9bd0185be
--- /dev/null
+++ b/packages/nodes-base/nodes/Okta/Okta.node.ts
@@ -0,0 +1,56 @@
+import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
+import { userFields, userOperations } from './UserDescription';
+import { getUsers } from './UserFunctions';
+
+export class Okta implements INodeType {
+ description: INodeTypeDescription = {
+ displayName: 'Okta',
+ name: 'okta',
+ icon: { light: 'file:Okta.svg', dark: 'file:Okta.dark.svg' },
+ group: ['transform'],
+ version: 1,
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
+ description: 'Use the Okta API',
+ defaults: {
+ name: 'Okta',
+ },
+ inputs: ['main'],
+ outputs: ['main'],
+ credentials: [
+ {
+ name: 'oktaApi',
+ required: true,
+ },
+ ],
+ requestDefaults: {
+ returnFullResponse: true,
+ baseURL: '={{$credentials.url.replace(new RegExp("/$"), "")}}',
+ headers: {},
+ },
+ properties: [
+ {
+ displayName: 'Resource',
+ name: 'resource',
+ type: 'options',
+ noDataExpression: true,
+ options: [
+ {
+ name: 'User',
+ value: 'user',
+ },
+ ],
+ default: 'user',
+ },
+
+ // USER
+ ...userOperations,
+ ...userFields,
+ ],
+ };
+
+ methods = {
+ listSearch: {
+ getUsers,
+ },
+ };
+}
diff --git a/packages/nodes-base/nodes/Okta/Okta.svg b/packages/nodes-base/nodes/Okta/Okta.svg
new file mode 100644
index 0000000000..4ba579621f
--- /dev/null
+++ b/packages/nodes-base/nodes/Okta/Okta.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/nodes-base/nodes/Okta/UserDescription.ts b/packages/nodes-base/nodes/Okta/UserDescription.ts
new file mode 100644
index 0000000000..b10d580339
--- /dev/null
+++ b/packages/nodes-base/nodes/Okta/UserDescription.ts
@@ -0,0 +1,795 @@
+import type { INodeProperties } from 'n8n-workflow';
+import { getCursorPaginator, simplifyGetAllResponse, simplifyGetResponse } from './UserFunctions';
+const BASE_API_URL = '/api/v1/users/';
+export const userOperations: INodeProperties[] = [
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ noDataExpression: true,
+ displayOptions: {
+ show: {
+ resource: ['user'],
+ },
+ },
+ options: [
+ // Create Operation
+ {
+ name: 'Create',
+ value: 'create',
+ description: 'Create a new user',
+ routing: {
+ request: {
+ method: 'POST',
+ url: BASE_API_URL,
+ qs: { activate: '={{$parameter["activate"]}}' },
+ returnFullResponse: true,
+ },
+ },
+ action: 'Create a new user',
+ },
+ // Delete Operation
+ {
+ name: 'Delete',
+ value: 'delete',
+ description: 'Delete an existing user',
+ routing: {
+ request: {
+ method: 'DELETE',
+ url: '={{"/api/v1/users/" + $parameter["userId"]}}',
+ returnFullResponse: true,
+ },
+ output: {
+ postReceive: [
+ {
+ type: 'set',
+ properties: {
+ value: '={{ { "success": true } }}',
+ },
+ },
+ ],
+ },
+ },
+ action: 'Delete a user',
+ },
+ // Get Operation
+ {
+ name: 'Get',
+ value: 'get',
+ description: 'Get details of a user',
+ routing: {
+ request: {
+ method: 'GET',
+ url: '={{"/api/v1/users/" + $parameter["userId"]}}',
+ returnFullResponse: true,
+ qs: {},
+ },
+ output: {
+ postReceive: [simplifyGetResponse],
+ },
+ },
+ action: 'Get a user',
+ },
+ // Get All Operation
+ {
+ name: 'Get Many',
+ value: 'getAll',
+ description: 'Get many users',
+ routing: {
+ request: {
+ method: 'GET',
+ url: BASE_API_URL,
+ qs: { search: '={{$parameter["searchQuery"]}}' },
+ returnFullResponse: true,
+ },
+ output: {
+ postReceive: [simplifyGetAllResponse],
+ },
+ send: {
+ paginate: true,
+ },
+ operations: {
+ pagination: getCursorPaginator(),
+ },
+ },
+ action: 'Get many users',
+ },
+ // Update Operation
+ {
+ name: 'Update',
+ value: 'update',
+ description: 'Update an existing user',
+ routing: {
+ request: {
+ method: 'POST',
+ url: '={{"/api/v1/users/" + $parameter["userId"]}}',
+ returnFullResponse: true,
+ },
+ },
+ action: 'Update a user',
+ },
+ ],
+ default: 'getAll',
+ },
+];
+const mainProfileFields: INodeProperties[] = [
+ {
+ displayName: 'First Name',
+ name: 'firstName',
+ type: 'string',
+ placeholder: 'e.g. Nathan',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.firstName',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Last Name',
+ name: 'lastName',
+ type: 'string',
+ placeholder: 'e.g. Smith',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.lastName',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Username',
+ name: 'login',
+ type: 'string',
+ placeholder: 'e.g. nathan@example.com',
+ hint: 'Unique identifier for the user, must be an email',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.login',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Email',
+ name: 'email',
+ type: 'string',
+ placeholder: 'e.g. nathan@example.com',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.email',
+ type: 'body',
+ },
+ },
+ },
+];
+const createFields: INodeProperties[] = [
+ {
+ displayName: 'City',
+ name: 'city',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.city',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Cost Center',
+ name: 'costCenter',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.costCenter',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Country Code',
+ name: 'countryCode',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.countryCode',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Department',
+ name: 'department',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.department',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Display Name',
+ name: 'displayName',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.displayName',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Division',
+ name: 'division',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.division',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Employee Number',
+ name: 'employeeNumber',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.employeeNumber',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Honorific Prefix',
+ name: 'honorificPrefix',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.honorificPrefix',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Honorific Suffix',
+ name: 'honorificSuffix',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.honorificSuffix',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Locale',
+ name: 'locale',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.locale',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Manager',
+ name: 'manager',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.manager',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'ManagerId',
+ name: 'managerId',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.managerId',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Middle Name',
+ name: 'middleName',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.middleName',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Mobile Phone',
+ name: 'mobilePhone',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.mobilePhone',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Nick Name',
+ name: 'nickName',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.nickName',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Password',
+ name: 'password',
+ type: 'string',
+ typeOptions: { password: true },
+ default: '',
+ routing: {
+ send: {
+ property: 'credentials.password.value',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Organization',
+ name: 'organization',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.organization',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Postal Address',
+ name: 'postalAddress',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.postalAddress',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Preferred Language',
+ name: 'preferredLanguage',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.preferredLanguage',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Primary Phone',
+ name: 'primaryPhone',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.primaryPhone',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Profile Url',
+ name: 'profileUrl',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.profileUrl',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Recovery Question Answer',
+ name: 'recoveryQuestionAnswer',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'credentials.recovery_question.answer',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Recovery Question Question',
+ name: 'recoveryQuestionQuestion',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'credentials.recovery_question.question',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Second Email',
+ name: 'secondEmail',
+ type: 'string',
+ typeOptions: { email: true },
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.secondEmail',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'State',
+ name: 'state',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.state',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Street Address',
+ name: 'streetAddress',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.streetAddress',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Timezone',
+ name: 'timezone',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.timezone',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Title',
+ name: 'title',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.title',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'User Type',
+ name: 'userType',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.userType',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Zip Code',
+ name: 'zipCode',
+ type: 'string',
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.zipCode',
+ type: 'body',
+ },
+ },
+ },
+];
+const updateFields: INodeProperties[] = createFields
+ .concat(mainProfileFields)
+ .sort((a, b) => a.displayName.localeCompare(b.displayName));
+
+export const userFields: INodeProperties[] = [
+ // Fields for 'get', 'update', and 'delete' operations
+
+ {
+ displayName: 'User',
+ name: 'userId',
+ type: 'resourceLocator',
+ default: { mode: 'list', value: '' },
+ required: true,
+ modes: [
+ {
+ displayName: 'From List',
+ name: 'list',
+ type: 'list',
+ placeholder: 'Select a user...',
+ typeOptions: {
+ searchListMethod: 'getUsers',
+ searchable: true,
+ },
+ },
+ {
+ displayName: 'By username',
+ name: 'login',
+ type: 'string',
+ placeholder: '',
+ },
+ {
+ displayName: 'ID',
+ name: 'id',
+ type: 'string',
+ placeholder: 'e.g. 00u1abcd2345EfGHIjk6',
+ },
+ ],
+ displayOptions: {
+ show: {
+ resource: ['user'],
+ operation: ['get', 'update', 'delete'],
+ },
+ },
+ description: 'The user you want to operate on. Choose from the list, or specify an ID.',
+ },
+ // Fields specific to 'create' operation
+ {
+ displayName: 'First Name',
+ name: 'firstName',
+ type: 'string',
+ required: true,
+ placeholder: 'e.g. Nathan',
+ displayOptions: {
+ show: {
+ resource: ['user'],
+ operation: ['create'],
+ },
+ },
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.firstName',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Last Name',
+ name: 'lastName',
+ type: 'string',
+ required: true,
+ placeholder: 'e.g. Smith',
+ displayOptions: {
+ show: {
+ resource: ['user'],
+ operation: ['create'],
+ },
+ },
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.lastName',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Username',
+ name: 'login',
+ type: 'string',
+ required: true,
+ placeholder: 'e.g. nathan@example.com',
+ hint: 'Unique identifier for the user, must be an email',
+ displayOptions: {
+ show: {
+ resource: ['user'],
+ operation: ['create'],
+ },
+ },
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.login',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Email',
+ name: 'email',
+ type: 'string',
+ required: true,
+ placeholder: 'e.g. nathan@example.com',
+ displayOptions: {
+ show: {
+ resource: ['user'],
+ operation: ['create'],
+ },
+ },
+ default: '',
+ routing: {
+ send: {
+ property: 'profile.email',
+ type: 'body',
+ },
+ },
+ },
+ {
+ displayName: 'Activate',
+ name: 'activate',
+ type: 'boolean',
+ displayOptions: {
+ show: {
+ resource: ['user'],
+ operation: ['create'],
+ },
+ },
+ default: true,
+ description: 'Whether to activate the user and allow access to all assigned applications',
+ },
+ {
+ displayName: 'Fields',
+ name: 'getCreateFields',
+ type: 'collection',
+ displayOptions: {
+ show: {
+ resource: ['user'],
+ operation: ['create'],
+ },
+ },
+ default: {},
+ placeholder: 'Add field',
+ options: createFields,
+ },
+
+ // Fields for 'update' operations
+ {
+ displayName: 'Fields',
+ name: 'getUpdateFields',
+ type: 'collection',
+ displayOptions: {
+ show: {
+ resource: ['user'],
+ operation: ['update'],
+ },
+ },
+ default: {},
+ placeholder: 'Add field',
+ options: updateFields,
+ },
+
+ // Fields specific to 'getAll' operation
+ {
+ displayName: 'Search Query',
+ name: 'searchQuery',
+ type: 'string',
+ placeholder: 'e.g. profile.lastName sw "Smi"',
+ hint: 'Filter users by using the allowed syntax. More info.',
+ displayOptions: {
+ show: {
+ resource: ['user'],
+ operation: ['getAll'],
+ },
+ },
+ default: '',
+ routing: {
+ request: {
+ qs: {
+ prefix: '={{$value}}',
+ },
+ },
+ },
+ },
+ {
+ displayName: 'Limit',
+ name: 'limit',
+ type: 'number',
+ displayOptions: {
+ show: {
+ resource: ['user'],
+ operation: ['getAll'],
+ returnAll: [false],
+ },
+ },
+ typeOptions: {
+ minValue: 1,
+ maxValue: 200,
+ },
+ default: 20,
+ routing: {
+ send: {
+ type: 'query',
+ property: 'limit',
+ },
+ output: {
+ maxResults: '={{$value}}', // Set maxResults to the value of current parameter
+ },
+ },
+ description: 'Max number of results to return',
+ },
+ {
+ displayName: 'Return All',
+ name: 'returnAll',
+ type: 'boolean',
+ displayOptions: {
+ show: {
+ resource: ['user'],
+ operation: ['getAll'],
+ },
+ },
+ default: false,
+ description: 'Whether to return all results or only up to a given limit',
+ },
+ // Fields for 'get' and 'getAll' operations
+ {
+ displayName: 'Simplify',
+ name: 'simplify',
+ type: 'boolean',
+ displayOptions: {
+ show: {
+ resource: ['user'],
+ operation: ['get', 'getAll'],
+ },
+ },
+ default: true,
+ description: 'Whether to return a simplified version of the response instead of the raw data',
+ },
+ // Fields specific to 'delete' operation
+ {
+ displayName: 'Send Email',
+ name: 'sendEmail',
+ type: 'boolean',
+ displayOptions: {
+ show: {
+ resource: ['user'],
+ operation: ['delete'],
+ },
+ },
+ default: false,
+ description: 'Whether to send a deactivation email to the administrator',
+ },
+];
diff --git a/packages/nodes-base/nodes/Okta/UserFunctions.ts b/packages/nodes-base/nodes/Okta/UserFunctions.ts
new file mode 100644
index 0000000000..6def8ea296
--- /dev/null
+++ b/packages/nodes-base/nodes/Okta/UserFunctions.ts
@@ -0,0 +1,170 @@
+import type {
+ DeclarativeRestApiSettings,
+ IDataObject,
+ IExecuteFunctions,
+ IExecutePaginationFunctions,
+ IExecuteSingleFunctions,
+ IHookFunctions,
+ IHttpRequestMethods,
+ IHttpRequestOptions,
+ ILoadOptionsFunctions,
+ IN8nHttpFullResponse,
+ INodeExecutionData,
+ INodeListSearchResult,
+ INodePropertyOptions,
+} from 'n8n-workflow';
+
+type OktaUser = {
+ status: string;
+ created: string;
+ activated: string;
+ lastLogin: string;
+ lastUpdated: string;
+ passwordChanged: string;
+ profile: {
+ login: string;
+ email: string;
+ firstName: string;
+ lastName: string;
+ };
+ id: string;
+};
+
+export async function oktaApiRequest(
+ this: IExecuteFunctions | IExecuteSingleFunctions | IHookFunctions | ILoadOptionsFunctions,
+ method: IHttpRequestMethods,
+ resource: string,
+ body: IDataObject = {},
+ qs: IDataObject = {},
+ url?: string,
+ option: IDataObject = {},
+): Promise {
+ const credentials = await this.getCredentials('oktaApi');
+ const baseUrl = `${credentials.url as string}/api/v1/${resource}`;
+ const options: IHttpRequestOptions = {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ method,
+ body: Object.keys(body).length ? body : undefined,
+ qs: Object.keys(qs).length ? qs : undefined,
+ url: url ?? baseUrl,
+ json: true,
+ ...option,
+ };
+ return await (this.helpers.httpRequestWithAuthentication.call(
+ this,
+ 'oktaApi',
+ options,
+ ) as Promise);
+}
+
+export async function getUsers(
+ this: ILoadOptionsFunctions,
+ filter?: string,
+): Promise {
+ const responseData: OktaUser[] = await oktaApiRequest.call(this, 'GET', '/users/');
+ const filteredUsers = responseData.filter((user) => {
+ if (!filter) return true;
+ const username = `${user.profile.login}`.toLowerCase();
+ return username.includes(filter.toLowerCase());
+ });
+ const users: INodePropertyOptions[] = filteredUsers.map((user) => ({
+ name: `${user.profile.login}`,
+ value: user.id,
+ }));
+ return {
+ results: users,
+ };
+}
+
+function simplifyOktaUser(item: OktaUser): IDataObject {
+ return {
+ id: item.id,
+ status: item.status,
+ created: item.created,
+ activated: item.activated,
+ lastLogin: item.lastLogin,
+ lastUpdated: item.lastUpdated,
+ passwordChanged: item.passwordChanged,
+ profile: {
+ firstName: item.profile.firstName,
+ lastName: item.profile.lastName,
+ login: item.profile.login,
+ email: item.profile.email,
+ },
+ };
+}
+
+export async function simplifyGetAllResponse(
+ this: IExecuteSingleFunctions,
+ items: INodeExecutionData[],
+ _response: IN8nHttpFullResponse,
+): Promise {
+ if (items.length === 0) return items;
+ const simplify = this.getNodeParameter('simplify');
+ if (!simplify)
+ return ((items[0].json as unknown as IDataObject[]) ?? []).map((item: IDataObject) => ({
+ json: item,
+ headers: _response.headers,
+ })) as INodeExecutionData[];
+ let simplifiedItems: INodeExecutionData[] = [];
+ if (items[0].json) {
+ const jsonArray = items[0].json as unknown;
+ simplifiedItems = (jsonArray as OktaUser[]).map((item: OktaUser) => {
+ const simplifiedItem = simplifyOktaUser(item);
+ return {
+ json: simplifiedItem,
+ headers: _response.headers,
+ };
+ });
+ }
+
+ return simplifiedItems;
+}
+
+export async function simplifyGetResponse(
+ this: IExecuteSingleFunctions,
+ items: INodeExecutionData[],
+ _response: IN8nHttpFullResponse,
+): Promise {
+ const simplify = this.getNodeParameter('simplify');
+ if (!simplify) return items;
+ const item = items[0].json as OktaUser;
+ const simplifiedItem = simplifyOktaUser(item);
+
+ return [
+ {
+ json: simplifiedItem,
+ },
+ ] as INodeExecutionData[];
+}
+
+export const getCursorPaginator = () => {
+ return async function cursorPagination(
+ this: IExecutePaginationFunctions,
+ requestOptions: DeclarativeRestApiSettings.ResultOptions,
+ ): Promise {
+ if (!requestOptions.options.qs) {
+ requestOptions.options.qs = {};
+ }
+
+ let items: INodeExecutionData[] = [];
+ let responseData: INodeExecutionData[];
+ let nextCursor: string | undefined = undefined;
+ const returnAll = this.getNodeParameter('returnAll', true) as boolean;
+ do {
+ requestOptions.options.qs.limit = 200;
+ requestOptions.options.qs.after = nextCursor;
+ responseData = await this.makeRoutingRequest(requestOptions);
+ if (responseData.length > 0) {
+ const headers = responseData[responseData.length - 1].headers;
+ const headersLink = (headers as IDataObject)?.link as string | undefined;
+ nextCursor = headersLink?.split('after=')[1]?.split('&')[0]?.split('>')[0];
+ }
+ items = items.concat(responseData);
+ } while (returnAll && nextCursor);
+
+ return items;
+ };
+};
diff --git a/packages/nodes-base/nodes/Okta/test/UserFunctions.test.ts b/packages/nodes-base/nodes/Okta/test/UserFunctions.test.ts
new file mode 100644
index 0000000000..9e5262db67
--- /dev/null
+++ b/packages/nodes-base/nodes/Okta/test/UserFunctions.test.ts
@@ -0,0 +1,375 @@
+import type {
+ DeclarativeRestApiSettings,
+ IDataObject,
+ IExecuteFunctions,
+ IExecutePaginationFunctions,
+ IExecuteSingleFunctions,
+ ILoadOptionsFunctions,
+ IN8nHttpFullResponse,
+ INodeExecutionData,
+} from 'n8n-workflow';
+import {
+ getCursorPaginator,
+ getUsers,
+ oktaApiRequest,
+ simplifyGetAllResponse,
+ simplifyGetResponse,
+} from '../UserFunctions';
+
+describe('oktaApiRequest', () => {
+ const mockGetCredentials = jest.fn();
+ const mockHttpRequestWithAuthentication = jest.fn();
+
+ const mockContext = {
+ getCredentials: mockGetCredentials,
+ helpers: {
+ httpRequestWithAuthentication: mockHttpRequestWithAuthentication,
+ },
+ } as unknown as IExecuteFunctions;
+
+ beforeEach(() => {
+ mockGetCredentials.mockClear();
+ mockHttpRequestWithAuthentication.mockClear();
+ });
+
+ it('should make a GET request and return data', async () => {
+ mockGetCredentials.mockResolvedValue({ url: 'https://okta.example.com' });
+ mockHttpRequestWithAuthentication.mockResolvedValue([
+ { profile: { firstName: 'John', lastName: 'Doe' }, id: '1' },
+ ]);
+
+ const response = await oktaApiRequest.call(mockContext, 'GET', 'users');
+
+ expect(mockGetCredentials).toHaveBeenCalledWith('oktaApi');
+ expect(mockHttpRequestWithAuthentication).toHaveBeenCalledWith('oktaApi', {
+ headers: { 'Content-Type': 'application/json' },
+ method: 'GET',
+ body: undefined,
+ qs: undefined,
+ url: 'https://okta.example.com/api/v1/users',
+ json: true,
+ });
+ expect(response).toEqual([{ profile: { firstName: 'John', lastName: 'Doe' }, id: '1' }]);
+ });
+
+ // Tests for error handling
+ it('should handle errors from oktaApiRequest', async () => {
+ mockHttpRequestWithAuthentication.mockRejectedValue(new Error('Network error'));
+
+ await expect(oktaApiRequest.call(mockContext, 'GET', 'users')).rejects.toThrow('Network error');
+ });
+});
+
+describe('getUsers', () => {
+ const mockOktaApiRequest = jest.fn();
+ const mockContext = {
+ getCredentials: jest.fn().mockResolvedValue({ url: 'https://okta.example.com' }),
+ helpers: {
+ httpRequestWithAuthentication: mockOktaApiRequest,
+ },
+ } as unknown as ILoadOptionsFunctions;
+
+ beforeEach(() => {
+ mockOktaApiRequest.mockClear();
+ });
+
+ it('should return users with filtering', async () => {
+ mockOktaApiRequest.mockResolvedValue([
+ { profile: { login: 'John@example.com' }, id: '1' },
+ { profile: { login: 'Jane@example.com' }, id: '2' },
+ ]);
+
+ const response = await getUsers.call(mockContext, 'john');
+
+ expect(response).toEqual({
+ results: [{ name: 'John@example.com', value: '1' }],
+ });
+ });
+
+ it('should return all users when no filter is applied', async () => {
+ mockOktaApiRequest.mockResolvedValue([
+ { profile: { login: 'John@example.com' }, id: '1' },
+ { profile: { login: 'Jane@example.com' }, id: '2' },
+ ]);
+
+ const response = await getUsers.call(mockContext);
+
+ expect(response).toEqual({
+ results: [
+ { name: 'John@example.com', value: '1' },
+ { name: 'Jane@example.com', value: '2' },
+ ],
+ });
+ });
+
+ // Tests for empty results
+ it('should handle empty results from oktaApiRequest', async () => {
+ mockOktaApiRequest.mockResolvedValue([]);
+
+ const response = await getUsers.call(mockContext);
+
+ expect(response).toEqual({
+ results: [],
+ });
+ });
+});
+
+describe('simplifyGetAllResponse', () => {
+ const mockGetNodeParameter = jest.fn();
+ const mockContext = {
+ getNodeParameter: mockGetNodeParameter,
+ } as unknown as IExecuteSingleFunctions;
+ const mockResponse = jest.fn() as unknown as IN8nHttpFullResponse;
+
+ const items: INodeExecutionData[] = [
+ {
+ json: [
+ {
+ id: '01',
+ status: 'ACTIVE',
+ created: '2023-01-01T00:00:00.000Z',
+ activated: '2023-01-01T00:00:01.000Z',
+ lastLogin: null,
+ lastUpdated: '2023-01-01T00:00:01.000Z',
+ passwordChanged: null,
+ some_item: 'some_value',
+ profile: {
+ firstName: 'John',
+ lastName: 'Doe',
+ login: 'john.doe@example.com',
+ email: 'john.doe@example.com',
+ some_profile_item: 'some_profile_value',
+ },
+ },
+ ] as unknown as IDataObject,
+ },
+ ];
+
+ beforeEach(() => {
+ mockGetNodeParameter.mockClear();
+ });
+
+ it('should return items unchanged when simplify parameter is not set', async () => {
+ mockGetNodeParameter.mockReturnValueOnce(false);
+
+ const expectedResult: INodeExecutionData[] = [
+ {
+ json: {
+ id: '01',
+ status: 'ACTIVE',
+ created: '2023-01-01T00:00:00.000Z',
+ activated: '2023-01-01T00:00:01.000Z',
+ lastLogin: null,
+ lastUpdated: '2023-01-01T00:00:01.000Z',
+ passwordChanged: null,
+ some_item: 'some_value',
+ profile: {
+ firstName: 'John',
+ lastName: 'Doe',
+ login: 'john.doe@example.com',
+ email: 'john.doe@example.com',
+ some_profile_item: 'some_profile_value',
+ },
+ },
+ },
+ ];
+
+ const result = await simplifyGetAllResponse.call(mockContext, items, mockResponse);
+ expect(result).toEqual(expectedResult);
+ });
+
+ it('should simplify items correctly', async () => {
+ mockGetNodeParameter.mockReturnValueOnce(true);
+
+ const expectedResult: INodeExecutionData[] = [
+ {
+ json: {
+ id: '01',
+ status: 'ACTIVE',
+ created: '2023-01-01T00:00:00.000Z',
+ activated: '2023-01-01T00:00:01.000Z',
+ lastLogin: null,
+ lastUpdated: '2023-01-01T00:00:01.000Z',
+ passwordChanged: null,
+ profile: {
+ firstName: 'John',
+ lastName: 'Doe',
+ login: 'john.doe@example.com',
+ email: 'john.doe@example.com',
+ },
+ },
+ },
+ ];
+
+ const result = await simplifyGetAllResponse.call(mockContext, items, mockResponse);
+ expect(result).toEqual(expectedResult);
+ });
+
+ it('should return an empty array when items is an empty array', async () => {
+ mockGetNodeParameter.mockReturnValueOnce(false);
+
+ const emptyArrayItems: INodeExecutionData[] = [];
+
+ const result = await simplifyGetAllResponse.call(mockContext, emptyArrayItems, mockResponse);
+ expect(result).toEqual([]);
+ });
+});
+
+describe('simplifyGetResponse', () => {
+ const mockGetNodeParameter = jest.fn();
+ const mockContext = {
+ getNodeParameter: mockGetNodeParameter,
+ } as unknown as IExecuteSingleFunctions;
+ const mockResponse = jest.fn() as unknown as IN8nHttpFullResponse;
+
+ const items: INodeExecutionData[] = [
+ {
+ json: {
+ id: '01',
+ status: 'ACTIVE',
+ created: '2023-01-01T00:00:00.000Z',
+ activated: '2023-01-01T00:00:01.000Z',
+ lastLogin: null,
+ lastUpdated: '2023-01-01T00:00:01.000Z',
+ passwordChanged: null,
+ some_item: 'some_value',
+ profile: {
+ firstName: 'John',
+ lastName: 'Doe',
+ login: 'john.doe@example.com',
+ email: 'john.doe@example.com',
+ some_profile_item: 'some_profile_value',
+ },
+ } as unknown as IDataObject,
+ },
+ ];
+ beforeEach(() => {
+ mockGetNodeParameter.mockClear();
+ });
+
+ it('should return the item unchanged when simplify parameter is not set', async () => {
+ mockGetNodeParameter.mockReturnValueOnce(false);
+
+ const expectedResult: INodeExecutionData[] = [
+ {
+ json: {
+ id: '01',
+ status: 'ACTIVE',
+ created: '2023-01-01T00:00:00.000Z',
+ activated: '2023-01-01T00:00:01.000Z',
+ lastLogin: null,
+ lastUpdated: '2023-01-01T00:00:01.000Z',
+ passwordChanged: null,
+ some_item: 'some_value',
+ profile: {
+ firstName: 'John',
+ lastName: 'Doe',
+ login: 'john.doe@example.com',
+ email: 'john.doe@example.com',
+ some_profile_item: 'some_profile_value',
+ },
+ },
+ },
+ ];
+
+ const result = await simplifyGetResponse.call(mockContext, items, mockResponse);
+ expect(result).toEqual(expectedResult);
+ });
+
+ it('should simplify the item correctly', async () => {
+ mockGetNodeParameter.mockReturnValueOnce(true);
+
+ const expectedResult: INodeExecutionData[] = [
+ {
+ json: {
+ id: '01',
+ status: 'ACTIVE',
+ created: '2023-01-01T00:00:00.000Z',
+ activated: '2023-01-01T00:00:01.000Z',
+ lastLogin: null,
+ lastUpdated: '2023-01-01T00:00:01.000Z',
+ passwordChanged: null,
+ profile: {
+ firstName: 'John',
+ lastName: 'Doe',
+ login: 'john.doe@example.com',
+ email: 'john.doe@example.com',
+ },
+ },
+ },
+ ];
+
+ const result = await simplifyGetResponse.call(mockContext, items, mockResponse);
+ expect(result).toEqual(expectedResult);
+ });
+});
+describe('getCursorPaginator', () => {
+ let mockContext: IExecutePaginationFunctions;
+ let mockRequestOptions: DeclarativeRestApiSettings.ResultOptions;
+ const baseUrl = 'https://api.example.com';
+
+ beforeEach(() => {
+ mockContext = {
+ getNodeParameter: jest.fn(),
+ makeRoutingRequest: jest.fn(),
+ } as unknown as IExecutePaginationFunctions;
+
+ mockRequestOptions = {
+ options: {
+ qs: {},
+ },
+ } as DeclarativeRestApiSettings.ResultOptions;
+ });
+
+ it('should return all items when returnAll is true', async () => {
+ const mockResponseData: INodeExecutionData[] = [
+ { json: { id: 1 }, headers: { link: `<${baseUrl}?after=cursor1>` } },
+ { json: { id: 2 }, headers: { link: `<${baseUrl}?after=cursor2>` } },
+ { json: { id: 3 }, headers: { link: `<${baseUrl}>` } },
+ ];
+
+ (mockContext.getNodeParameter as jest.Mock).mockReturnValue(true);
+ (mockContext.makeRoutingRequest as jest.Mock)
+ .mockResolvedValueOnce([mockResponseData[0]])
+ .mockResolvedValueOnce([mockResponseData[1]])
+ .mockResolvedValueOnce([mockResponseData[2]]);
+
+ const paginator = getCursorPaginator().bind(mockContext);
+ const result = await paginator(mockRequestOptions);
+
+ expect(result).toEqual(mockResponseData);
+ expect(mockContext.getNodeParameter).toHaveBeenCalledWith('returnAll', true);
+ expect(mockContext.makeRoutingRequest).toHaveBeenCalledTimes(3);
+ });
+
+ it('should return items until nextCursor is undefined', async () => {
+ const mockResponseData: INodeExecutionData[] = [
+ { json: { id: 1 }, headers: { link: `<${baseUrl}?after=cursor1>` } },
+ { json: { id: 2 }, headers: { link: `<${baseUrl}>` } },
+ ];
+
+ (mockContext.getNodeParameter as jest.Mock).mockReturnValue(true);
+ (mockContext.makeRoutingRequest as jest.Mock)
+ .mockResolvedValueOnce([mockResponseData[0]])
+ .mockResolvedValueOnce([mockResponseData[1]]);
+
+ const paginator = getCursorPaginator().bind(mockContext);
+ const result = await paginator(mockRequestOptions);
+
+ expect(result).toEqual(mockResponseData);
+ expect(mockContext.getNodeParameter).toHaveBeenCalledWith('returnAll', true);
+ expect(mockContext.makeRoutingRequest).toHaveBeenCalledTimes(2);
+ });
+
+ it('should handle empty response data', async () => {
+ (mockContext.getNodeParameter as jest.Mock).mockReturnValue(true);
+ (mockContext.makeRoutingRequest as jest.Mock).mockResolvedValue([]);
+
+ const paginator = getCursorPaginator().bind(mockContext);
+ const result = await paginator(mockRequestOptions);
+
+ expect(result).toEqual([]);
+ expect(mockContext.getNodeParameter).toHaveBeenCalledWith('returnAll', true);
+ expect(mockContext.makeRoutingRequest).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json
index 61f940d5cd..5c36310965 100644
--- a/packages/nodes-base/package.json
+++ b/packages/nodes-base/package.json
@@ -648,6 +648,7 @@
"dist/nodes/Notion/NotionTrigger.node.js",
"dist/nodes/Npm/Npm.node.js",
"dist/nodes/Odoo/Odoo.node.js",
+ "dist/nodes/Okta/Okta.node.js",
"dist/nodes/OneSimpleApi/OneSimpleApi.node.js",
"dist/nodes/OpenAi/OpenAi.node.js",
"dist/nodes/OpenThesaurus/OpenThesaurus.node.js",
diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts
index c4607563db..eb6c3ca07c 100644
--- a/packages/workflow/src/Interfaces.ts
+++ b/packages/workflow/src/Interfaces.ts
@@ -308,6 +308,7 @@ export interface ICredentialTestRequestData {
type ICredentialHttpRequestNode = {
name: string;
docsUrl: string;
+ hidden?: boolean;
} & ({ apiBaseUrl: string } | { apiBaseUrlPlaceholder: string });
export interface ICredentialType {