mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(editor): Normalize credential data from source control (#15434)
This commit is contained in:
committed by
GitHub
parent
e68149bbc7
commit
840a3bee4b
@@ -37,8 +37,8 @@ import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import type { Project, ProjectSharingData } from '@/types/projects.types';
|
||||
import { assert } from '@n8n/utils/assert';
|
||||
import type { IMenuItem } from '@n8n/design-system';
|
||||
import { assert } from '@n8n/utils/assert';
|
||||
import { createEventBus } from '@n8n/utils/event-bus';
|
||||
|
||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||
@@ -476,6 +476,19 @@ function getCredentialProperties(name: string): INodeProperties[] {
|
||||
return combineProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* We might get credential with empty parameters from source-control
|
||||
* which breaks our types and Fe checks
|
||||
*/
|
||||
function removePropertiesWithEmptyStrings<T extends { [key: string]: unknown }>(data: T): T {
|
||||
const copy = structuredClone(data);
|
||||
Object.entries(copy).forEach(([key, value]) => {
|
||||
if (value === '') delete copy[key];
|
||||
});
|
||||
return copy;
|
||||
}
|
||||
|
||||
async function loadCurrentCredential() {
|
||||
credentialId.value = props.activeId ?? '';
|
||||
|
||||
@@ -494,7 +507,10 @@ async function loadCurrentCredential() {
|
||||
|
||||
currentCredential.value = currentCredentials;
|
||||
|
||||
credentialData.value = (currentCredentials.data as ICredentialDataDecryptedObject) || {};
|
||||
credentialData.value = removePropertiesWithEmptyStrings(
|
||||
(currentCredentials.data as ICredentialDataDecryptedObject) || {},
|
||||
);
|
||||
|
||||
if (currentCredentials.sharedWithProjects) {
|
||||
credentialData.value = {
|
||||
...credentialData.value,
|
||||
|
||||
@@ -2,9 +2,182 @@ import { createComponentRenderer } from '@/__tests__/render';
|
||||
import CredentialEdit from '@/components/CredentialEdit/CredentialEdit.vue';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { CREDENTIAL_EDIT_MODAL_KEY, STORES } from '@/constants';
|
||||
import { cleanupAppModals, createAppModals, retry } from '@/__tests__/utils';
|
||||
import { cleanupAppModals, createAppModals, retry, mockedStore } from '@/__tests__/utils';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import type { ICredentialsResponse } from '@/Interface';
|
||||
import { within } from '@testing-library/vue';
|
||||
import type { ICredentialType } from 'n8n-workflow';
|
||||
|
||||
const oAuth2Api: ICredentialType = {
|
||||
name: 'oAuth2Api',
|
||||
displayName: 'OAuth2 API',
|
||||
documentationUrl: 'httpRequest',
|
||||
genericAuth: true,
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Grant Type',
|
||||
name: 'grantType',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Authorization Code',
|
||||
value: 'authorizationCode',
|
||||
},
|
||||
{
|
||||
name: 'Client Credentials',
|
||||
value: 'clientCredentials',
|
||||
},
|
||||
{
|
||||
name: 'PKCE',
|
||||
value: 'pkce',
|
||||
},
|
||||
],
|
||||
default: 'authorizationCode',
|
||||
},
|
||||
{
|
||||
displayName: 'Authorization URL',
|
||||
name: 'authUrl',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
grantType: ['authorizationCode', 'pkce'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Access Token URL',
|
||||
name: 'accessTokenUrl',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Client ID',
|
||||
name: 'clientId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Client Secret',
|
||||
name: 'clientSecret',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
password: true,
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Scope',
|
||||
name: 'scope',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Auth URI Query Parameters',
|
||||
name: 'authQueryParameters',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
grantType: ['authorizationCode', 'pkce'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description:
|
||||
'For some services additional query parameters have to be set which can be defined here',
|
||||
placeholder: 'access_type=offline',
|
||||
},
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Body',
|
||||
value: 'body',
|
||||
description: 'Send credentials in body',
|
||||
},
|
||||
{
|
||||
name: 'Header',
|
||||
value: 'header',
|
||||
description: 'Send credentials as Basic Auth header',
|
||||
},
|
||||
],
|
||||
default: 'header',
|
||||
},
|
||||
{
|
||||
displayName: 'Ignore SSL Issues (Insecure)',
|
||||
name: 'ignoreSSLIssues',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
doNotInherit: true,
|
||||
},
|
||||
],
|
||||
iconUrl: 'icons/n8n-nodes-base/dist/nodes/GraphQL/graphql.png',
|
||||
supportedNodes: ['n8n-nodes-base.graphql', 'n8n-nodes-base.httpRequest'],
|
||||
};
|
||||
|
||||
const googleOAuth2Api: ICredentialType = {
|
||||
name: 'googleOAuth2Api',
|
||||
extends: ['oAuth2Api'],
|
||||
displayName: 'Google OAuth2 API',
|
||||
documentationUrl: 'google/oauth-generic',
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Grant Type',
|
||||
name: 'grantType',
|
||||
type: 'hidden',
|
||||
default: 'authorizationCode',
|
||||
},
|
||||
{
|
||||
displayName: 'Authorization URL',
|
||||
name: 'authUrl',
|
||||
type: 'hidden',
|
||||
default: 'https://accounts.google.com/o/oauth2/v2/auth',
|
||||
},
|
||||
{
|
||||
displayName: 'Access Token URL',
|
||||
name: 'accessTokenUrl',
|
||||
type: 'hidden',
|
||||
default: 'https://oauth2.googleapis.com/token',
|
||||
},
|
||||
{
|
||||
displayName: 'Auth URI Query Parameters',
|
||||
name: 'authQueryParameters',
|
||||
type: 'hidden',
|
||||
default: 'access_type=offline&prompt=consent',
|
||||
},
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'hidden',
|
||||
default: 'body',
|
||||
},
|
||||
],
|
||||
iconUrl: 'icons/n8n-nodes-base/dist/credentials/icons/Google.svg',
|
||||
supportedNodes: [],
|
||||
};
|
||||
|
||||
const googleBigQueryOAuth2Api: ICredentialType = {
|
||||
name: 'googleBigQueryOAuth2Api',
|
||||
extends: ['googleOAuth2Api'],
|
||||
displayName: 'Google BigQuery OAuth2 API',
|
||||
documentationUrl: 'google/oauth-single-service',
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Scope',
|
||||
name: 'scope',
|
||||
type: 'hidden',
|
||||
default:
|
||||
'https://www.googleapis.com/auth/bigquery https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/drive',
|
||||
},
|
||||
],
|
||||
iconUrl: 'icons/n8n-nodes-base/dist/nodes/Google/BigQuery/googleBigQuery.svg',
|
||||
supportedNodes: ['n8n-nodes-base.googleBigQuery'],
|
||||
};
|
||||
|
||||
vi.mock('@/permissions', () => ({
|
||||
getResourcePermissions: vi.fn(() => ({
|
||||
@@ -123,4 +296,62 @@ describe('CredentialEdit', () => {
|
||||
await retry(() => expect(queryByText('Connection')).not.toBeInTheDocument());
|
||||
await retry(() => expect(queryByText('Sharing')).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
case: 'valid credential',
|
||||
data: {
|
||||
clientId: 'client_id',
|
||||
clientSecret: '__n8n_EMPTY_VALUE_7b1af746-3729-4c60-9b9b-e08eb29e58da',
|
||||
},
|
||||
},
|
||||
{
|
||||
case: 'malformed credential from source-control',
|
||||
data: {
|
||||
grantType: '',
|
||||
authUrl: '',
|
||||
accessTokenUrl: '',
|
||||
clientId: 'client_id',
|
||||
clientSecret: '__n8n_EMPTY_VALUE_7b1af746-3729-4c60-9b9b-e08eb29e58da',
|
||||
scope: '',
|
||||
authQueryParameters: '',
|
||||
authentication: '',
|
||||
},
|
||||
},
|
||||
])('should show Sign in with Google for $case data', async ({ data }) => {
|
||||
const credentialsStore = mockedStore(useCredentialsStore);
|
||||
credentialsStore.getCredentialData.mockResolvedValueOnce({
|
||||
// @ts-expect-error data is decrypted
|
||||
data,
|
||||
createdAt: '2025-05-13T10:45:45.588Z',
|
||||
updatedAt: '2025-05-15T12:36:01.826Z',
|
||||
id: '6ufuMkOc7bb3LyGV',
|
||||
name: 'Google BigQuery account',
|
||||
type: 'googleBigQueryOAuth2Api',
|
||||
isManaged: false,
|
||||
sharedWithProjects: [],
|
||||
scopes: ['credential:update'],
|
||||
oauthTokenData: false,
|
||||
});
|
||||
|
||||
credentialsStore.state.credentialTypes = {
|
||||
[oAuth2Api.name]: oAuth2Api,
|
||||
[googleOAuth2Api.name]: googleOAuth2Api,
|
||||
[googleBigQueryOAuth2Api.name]: googleBigQueryOAuth2Api,
|
||||
};
|
||||
|
||||
const { getByTestId } = renderComponent({
|
||||
props: {
|
||||
activeId: '6ufuMkOc7bb3LyGV',
|
||||
modalName: CREDENTIAL_EDIT_MODAL_KEY,
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
await retry(() => expect(credentialsStore.getCredentialData).toHaveBeenCalled());
|
||||
await retry(() => expect(getByTestId('credential-edit-dialog')).toBeInTheDocument());
|
||||
|
||||
expect(
|
||||
within(getByTestId('credential-edit-dialog')).getByTestId('oauth-connect-button'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user