mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-19 11:01:15 +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 { useUIStore } from '@/stores/ui.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import type { Project, ProjectSharingData } from '@/types/projects.types';
|
import type { Project, ProjectSharingData } from '@/types/projects.types';
|
||||||
import { assert } from '@n8n/utils/assert';
|
|
||||||
import type { IMenuItem } from '@n8n/design-system';
|
import type { IMenuItem } from '@n8n/design-system';
|
||||||
|
import { assert } from '@n8n/utils/assert';
|
||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
|
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
@@ -476,6 +476,19 @@ function getCredentialProperties(name: string): INodeProperties[] {
|
|||||||
return combineProperties;
|
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() {
|
async function loadCurrentCredential() {
|
||||||
credentialId.value = props.activeId ?? '';
|
credentialId.value = props.activeId ?? '';
|
||||||
|
|
||||||
@@ -494,7 +507,10 @@ async function loadCurrentCredential() {
|
|||||||
|
|
||||||
currentCredential.value = currentCredentials;
|
currentCredential.value = currentCredentials;
|
||||||
|
|
||||||
credentialData.value = (currentCredentials.data as ICredentialDataDecryptedObject) || {};
|
credentialData.value = removePropertiesWithEmptyStrings(
|
||||||
|
(currentCredentials.data as ICredentialDataDecryptedObject) || {},
|
||||||
|
);
|
||||||
|
|
||||||
if (currentCredentials.sharedWithProjects) {
|
if (currentCredentials.sharedWithProjects) {
|
||||||
credentialData.value = {
|
credentialData.value = {
|
||||||
...credentialData.value,
|
...credentialData.value,
|
||||||
|
|||||||
@@ -2,9 +2,182 @@ import { createComponentRenderer } from '@/__tests__/render';
|
|||||||
import CredentialEdit from '@/components/CredentialEdit/CredentialEdit.vue';
|
import CredentialEdit from '@/components/CredentialEdit/CredentialEdit.vue';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { CREDENTIAL_EDIT_MODAL_KEY, STORES } from '@/constants';
|
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 { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
import type { ICredentialsResponse } from '@/Interface';
|
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', () => ({
|
vi.mock('@/permissions', () => ({
|
||||||
getResourcePermissions: vi.fn(() => ({
|
getResourcePermissions: vi.fn(() => ({
|
||||||
@@ -123,4 +296,62 @@ describe('CredentialEdit', () => {
|
|||||||
await retry(() => expect(queryByText('Connection')).not.toBeInTheDocument());
|
await retry(() => expect(queryByText('Connection')).not.toBeInTheDocument());
|
||||||
await retry(() => expect(queryByText('Sharing')).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