mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-20 19:32:15 +00:00
⚡ Change credentials structure (#2139)
* ✨ change FE to handle new object type * 🚸 improve UX of handling invalid credentials * 🚧 WIP * 🎨 fix typescript issues * 🐘 add migrations for all supported dbs * ✏️ add description to migrations * ⚡ add credential update on import * ⚡ resolve after merge issues * 👕 fix lint issues * ⚡ check credentials on workflow create/update * update interface * 👕 fix ts issues * ⚡ adaption to new credentials UI * 🐛 intialize cache on BE for credentials check * 🐛 fix undefined oldCredentials * 🐛 fix deleting credential * 🐛 fix check for undefined keys * 🐛 fix disabling edit in execution * 🎨 just show credential name on execution view * ✏️ remove TODO * ⚡ implement review suggestions * ⚡ add cache to getCredentialsByType * ⏪ use getter instead of cache * ✏️ fix variable name typo * 🐘 include waiting nodes to migrations * 🐛 fix reverting migrations command * ⚡ update typeorm command * ✨ create db:revert command * 👕 fix lint error Co-authored-by: Mutasem <mutdmour@gmail.com>
This commit is contained in:
@@ -249,7 +249,7 @@ export interface IActivationError {
|
||||
}
|
||||
|
||||
export interface ICredentialsResponse extends ICredentialsEncrypted {
|
||||
id?: string;
|
||||
id: string;
|
||||
createdAt: number | string;
|
||||
updatedAt: number | string;
|
||||
}
|
||||
|
||||
@@ -557,6 +557,7 @@ export default mixins(showMessage, nodeHelpers).extend({
|
||||
);
|
||||
|
||||
const details: ICredentialsDecrypted = {
|
||||
id: this.credentialId,
|
||||
name: this.credentialName,
|
||||
type: this.credentialTypeName!,
|
||||
data: data as unknown as ICredentialDataDecryptedObject,
|
||||
@@ -605,6 +606,7 @@ export default mixins(showMessage, nodeHelpers).extend({
|
||||
);
|
||||
|
||||
const credentialDetails: ICredentialsDecrypted = {
|
||||
id: this.credentialId,
|
||||
name: this.credentialName,
|
||||
type: this.credentialTypeName!,
|
||||
data: data as unknown as ICredentialDataDecryptedObject,
|
||||
|
||||
@@ -9,15 +9,15 @@
|
||||
<el-col :span="10" class="parameter-name">
|
||||
{{credentialTypeNames[credentialTypeDescription.name]}}:
|
||||
</el-col>
|
||||
<el-col :span="12" class="parameter-value" :class="getIssues(credentialTypeDescription.name).length?'has-issues':''">
|
||||
|
||||
<el-col v-if="!isReadOnly" :span="12" class="parameter-value" :class="getIssues(credentialTypeDescription.name).length?'has-issues':''">
|
||||
<div :style="credentialInputWrapperStyle(credentialTypeDescription.name)">
|
||||
<n8n-select :value="selected[credentialTypeDescription.name]" :disabled="isReadOnly" @change="(value) => credentialSelected(credentialTypeDescription.name, value)" placeholder="Select Credential" size="small">
|
||||
<n8n-select :value="getSelectedId(credentialTypeDescription.name)" @change="(value) => onCredentialSelected(credentialTypeDescription.name, value)" placeholder="Select Credential" size="small">
|
||||
<n8n-option
|
||||
v-for="(item) in credentialOptions[credentialTypeDescription.name]"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.name">
|
||||
:value="item.id">
|
||||
</n8n-option>
|
||||
<n8n-option
|
||||
:key="NEW_CREDENTIALS_TEXT"
|
||||
@@ -34,10 +34,13 @@
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
|
||||
</el-col>
|
||||
<el-col :span="2" class="parameter-value credential-action">
|
||||
<font-awesome-icon v-if="selected[credentialTypeDescription.name] && isCredentialValid(credentialTypeDescription.name)" icon="pen" @click="editCredential(credentialTypeDescription.name)" class="update-credentials clickable" title="Update Credentials" />
|
||||
<el-col v-if="!isReadOnly" :span="2" class="parameter-value credential-action">
|
||||
<font-awesome-icon v-if="isCredentialExisting(credentialTypeDescription.name)" icon="pen" @click="editCredential(credentialTypeDescription.name)" class="update-credentials clickable" title="Update Credentials" />
|
||||
</el-col>
|
||||
|
||||
<el-col v-if="isReadOnly" :span="14" class="readonly-container" >
|
||||
<n8n-input disabled :value="selected && selected[credentialTypeDescription.name] && selected[credentialTypeDescription.name].name" size="small" />
|
||||
</el-col>
|
||||
|
||||
</el-row>
|
||||
@@ -49,12 +52,14 @@
|
||||
<script lang="ts">
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import {
|
||||
ICredentialsResponse,
|
||||
INodeUi,
|
||||
INodeUpdatePropertiesInformation,
|
||||
} from '@/Interface';
|
||||
import {
|
||||
ICredentialType,
|
||||
INodeCredentialDescription,
|
||||
INodeCredentialsDetails,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
@@ -119,11 +124,17 @@ export default mixins(
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
selected(): {[type: string]: string} {
|
||||
selected(): {[type: string]: INodeCredentialsDetails} {
|
||||
return this.node.credentials || {};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getSelectedId(type: string) {
|
||||
if (this.isCredentialExisting(type)) {
|
||||
return this.selected[type].id;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
credentialInputWrapperStyle (credentialType: string) {
|
||||
let deductWidth = 0;
|
||||
const styles = {
|
||||
@@ -145,10 +156,10 @@ export default mixins(
|
||||
|
||||
this.newCredentialUnsubscribe = this.$store.subscribe((mutation, state) => {
|
||||
if (mutation.type === 'credentials/upsertCredential' || mutation.type === 'credentials/enableOAuthCredential'){
|
||||
this.credentialSelected(credentialType, mutation.payload.name);
|
||||
this.onCredentialSelected(credentialType, mutation.payload.id);
|
||||
}
|
||||
if (mutation.type === 'credentials/deleteCredential') {
|
||||
this.credentialSelected(credentialType, mutation.payload.name);
|
||||
this.clearSelectedCredential(credentialType);
|
||||
this.stopListeningForNewCredentials();
|
||||
}
|
||||
});
|
||||
@@ -160,14 +171,52 @@ export default mixins(
|
||||
}
|
||||
},
|
||||
|
||||
credentialSelected (credentialType: string, credentialName: string) {
|
||||
clearSelectedCredential(credentialType: string) {
|
||||
const node: INodeUi = this.node;
|
||||
|
||||
const credentials = {
|
||||
...(node.credentials || {}),
|
||||
};
|
||||
|
||||
delete credentials[credentialType];
|
||||
|
||||
const updateInformation: INodeUpdatePropertiesInformation = {
|
||||
name: this.node.name,
|
||||
properties: {
|
||||
credentials,
|
||||
},
|
||||
};
|
||||
|
||||
this.$emit('credentialSelected', updateInformation);
|
||||
},
|
||||
|
||||
onCredentialSelected (credentialType: string, credentialId: string | null | undefined) {
|
||||
let selected = undefined;
|
||||
if (credentialName === NEW_CREDENTIALS_TEXT) {
|
||||
if (credentialId === NEW_CREDENTIALS_TEXT) {
|
||||
this.listenForNewCredentials(credentialType);
|
||||
this.$store.dispatch('ui/openNewCredential', { type: credentialType });
|
||||
return;
|
||||
}
|
||||
else {
|
||||
selected = credentialName;
|
||||
|
||||
const selectedCredentials = this.$store.getters['credentials/getCredentialById'](credentialId);
|
||||
const oldCredentials = this.node.credentials && this.node.credentials[credentialType] ? this.node.credentials[credentialType] : {};
|
||||
|
||||
selected = { id: selectedCredentials.id, name: selectedCredentials.name };
|
||||
|
||||
// if credentials has been string or neither id matched nor name matched uniquely
|
||||
if (oldCredentials.id === null || (oldCredentials.id && !this.$store.getters['credentials/getCredentialByIdAndType'](oldCredentials.id, credentialType))) {
|
||||
// update all nodes in the workflow with the same old/invalid credentials
|
||||
this.$store.commit('replaceInvalidWorkflowCredentials', {
|
||||
credentials: selected,
|
||||
invalid: oldCredentials,
|
||||
type: credentialType,
|
||||
});
|
||||
this.updateNodesCredentialsIssues();
|
||||
this.$showMessage({
|
||||
title: 'Node credentials updated',
|
||||
message: `Nodes that used credentials "${oldCredentials.name}" have been updated to use "${selected.name}"`,
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
const node: INodeUi = this.node;
|
||||
@@ -209,18 +258,19 @@ export default mixins(
|
||||
return node.issues.credentials[credentialTypeName];
|
||||
},
|
||||
|
||||
isCredentialValid(credentialType: string): boolean {
|
||||
const name = this.node.credentials[credentialType];
|
||||
isCredentialExisting(credentialType: string): boolean {
|
||||
if (!this.node.credentials || !this.node.credentials[credentialType] || !this.node.credentials[credentialType].id) {
|
||||
return false;
|
||||
}
|
||||
const { id } = this.node.credentials[credentialType];
|
||||
const options = this.credentialOptions[credentialType];
|
||||
|
||||
return options.find((option: ICredentialType) => option.name === name);
|
||||
return !!options.find((option: ICredentialsResponse) => option.id === id);
|
||||
},
|
||||
|
||||
editCredential(credentialType: string): void {
|
||||
const name = this.node.credentials[credentialType];
|
||||
const options = this.credentialOptions[credentialType];
|
||||
const selected = options.find((option: ICredentialType) => option.name === name);
|
||||
this.$store.dispatch('ui/openExisitngCredential', { id: selected.id });
|
||||
const { id } = this.node.credentials[credentialType];
|
||||
this.$store.dispatch('ui/openExisitngCredential', { id });
|
||||
|
||||
this.listenForNewCredentials(credentialType);
|
||||
},
|
||||
@@ -283,6 +333,10 @@ export default mixins(
|
||||
align-items: center;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
|
||||
.readonly-container {
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -7,11 +7,12 @@ import {
|
||||
ICredentialType,
|
||||
INodeCredentialDescription,
|
||||
NodeHelpers,
|
||||
INodeParameters,
|
||||
INodeCredentialsDetails,
|
||||
INodeExecutionData,
|
||||
INodeIssues,
|
||||
INodeIssueData,
|
||||
INodeIssueObjectProperty,
|
||||
INodeParameters,
|
||||
INodeProperties,
|
||||
INodeTypeDescription,
|
||||
IRunData,
|
||||
@@ -196,7 +197,7 @@ export const nodeHelpers = mixins(
|
||||
let userCredentials: ICredentialsResponse[] | null;
|
||||
let credentialType: ICredentialType | null;
|
||||
let credentialDisplayName: string;
|
||||
let selectedCredentials: string;
|
||||
let selectedCredentials: INodeCredentialsDetails;
|
||||
for (const credentialTypeDescription of nodeType!.credentials!) {
|
||||
// Check if credentials should be displayed else ignore
|
||||
if (this.displayParameter(node.parameters, credentialTypeDescription, '') !== true) {
|
||||
@@ -218,15 +219,35 @@ export const nodeHelpers = mixins(
|
||||
}
|
||||
} else {
|
||||
// If they are set check if the value is valid
|
||||
selectedCredentials = node.credentials[credentialTypeDescription.name];
|
||||
selectedCredentials = node.credentials[credentialTypeDescription.name] as INodeCredentialsDetails;
|
||||
if (typeof selectedCredentials === 'string') {
|
||||
selectedCredentials = {
|
||||
id: null,
|
||||
name: selectedCredentials,
|
||||
};
|
||||
}
|
||||
|
||||
userCredentials = this.$store.getters['credentials/getCredentialsByType'](credentialTypeDescription.name);
|
||||
|
||||
if (userCredentials === null) {
|
||||
userCredentials = [];
|
||||
}
|
||||
|
||||
if (userCredentials.find((credentialData) => credentialData.name === selectedCredentials) === undefined) {
|
||||
foundIssues[credentialTypeDescription.name] = [`Credentials with name "${selectedCredentials}" do not exist for "${credentialDisplayName}".`];
|
||||
if (selectedCredentials.id) {
|
||||
const idMatch = userCredentials.find((credentialData) => credentialData.id === selectedCredentials.id);
|
||||
if (idMatch) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const nameMatches = userCredentials.filter((credentialData) => credentialData.name === selectedCredentials.name);
|
||||
if (nameMatches.length > 1) {
|
||||
foundIssues[credentialTypeDescription.name] = [`Credentials with name "${selectedCredentials.name}" exist for "${credentialDisplayName}"`, "Credentials are not clearly identified. Please select the correct credentials."];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nameMatches.length === 0) {
|
||||
foundIssues[credentialTypeDescription.name] = [`Credentials with name "${selectedCredentials.name}" do not exist for "${credentialDisplayName}".`, "You can create credentials with the exact name and then they get auto-selected on refresh."];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,9 +90,15 @@ const module: Module<ICredentialsState, IRootState> = {
|
||||
getCredentialById: (state: ICredentialsState) => {
|
||||
return (id: string) => state.credentials[id];
|
||||
},
|
||||
getCredentialByIdAndType: (state: ICredentialsState) => {
|
||||
return (id: string, type: string) => {
|
||||
const credential = state.credentials[id];
|
||||
return !credential || credential.type !== type ? undefined : credential;
|
||||
};
|
||||
},
|
||||
getCredentialsByType: (state: ICredentialsState, getters: any) => { // tslint:disable-line:no-any
|
||||
return (credentialType: string): ICredentialsResponse[] => {
|
||||
return getters.allCredentials.filter((credentialData: ICredentialsResponse) => credentialData.type === credentialType);
|
||||
return getters.allCredentialsByType[credentialType];
|
||||
};
|
||||
},
|
||||
getNodesWithAccess (state: ICredentialsState, getters: any, rootState: IRootState, rootGetters: any) { // tslint:disable-line:no-any
|
||||
|
||||
@@ -375,6 +375,32 @@ export const store = new Vuex.Store({
|
||||
state.workflow.name = data.newName;
|
||||
},
|
||||
|
||||
// replace invalid credentials in workflow
|
||||
replaceInvalidWorkflowCredentials(state, {credentials, invalid, type }) {
|
||||
state.workflow.nodes.forEach((node) => {
|
||||
if (!node.credentials || !node.credentials[type]) {
|
||||
return;
|
||||
}
|
||||
const nodeCredentials = node.credentials[type];
|
||||
|
||||
if (typeof nodeCredentials === 'string' && nodeCredentials === invalid.name) {
|
||||
node.credentials[type] = credentials;
|
||||
return;
|
||||
}
|
||||
|
||||
if (nodeCredentials.id === null) {
|
||||
if (nodeCredentials.name === invalid.name){
|
||||
node.credentials[type] = credentials;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (nodeCredentials.id === invalid.id) {
|
||||
node.credentials[type] = credentials;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Nodes
|
||||
addNode (state, nodeData: INodeUi) {
|
||||
if (!nodeData.hasOwnProperty('name')) {
|
||||
|
||||
@@ -147,9 +147,11 @@ import {
|
||||
NodeHelpers,
|
||||
Workflow,
|
||||
IRun,
|
||||
INodeCredentialsDetails,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
IConnectionsUi,
|
||||
ICredentialsResponse,
|
||||
IExecutionResponse,
|
||||
IN8nUISettings,
|
||||
IWorkflowDb,
|
||||
@@ -327,6 +329,7 @@ export default mixins(
|
||||
ctrlKeyPressed: false,
|
||||
stopExecutionInProgress: false,
|
||||
blankRedirect: false,
|
||||
credentialsUpdated: false,
|
||||
};
|
||||
},
|
||||
beforeDestroy () {
|
||||
@@ -495,8 +498,10 @@ export default mixins(
|
||||
this.$store.commit('setWorkflowTagIds', tagIds || []);
|
||||
|
||||
await this.addNodes(data.nodes, data.connections);
|
||||
if (!this.credentialsUpdated) {
|
||||
this.$store.commit('setStateDirty', false);
|
||||
}
|
||||
|
||||
this.$store.commit('setStateDirty', false);
|
||||
this.zoomToFit();
|
||||
|
||||
this.$externalHooks().run('workflow.open', { workflowId, workflowName: data.name });
|
||||
@@ -1871,6 +1876,47 @@ export default mixins(
|
||||
this.deselectAllNodes();
|
||||
this.nodeSelectedByName(newName);
|
||||
},
|
||||
matchCredentials(node: INodeUi) {
|
||||
if (!node.credentials) {
|
||||
return;
|
||||
}
|
||||
Object.entries(node.credentials).forEach(([nodeCredentialType, nodeCredentials]: [string, INodeCredentialsDetails]) => {
|
||||
const credentialOptions = this.$store.getters['credentials/getCredentialsByType'](nodeCredentialType) as ICredentialsResponse[];
|
||||
|
||||
// Check if workflows applies old credentials style
|
||||
if (typeof nodeCredentials === 'string') {
|
||||
nodeCredentials = {
|
||||
id: null,
|
||||
name: nodeCredentials,
|
||||
};
|
||||
this.credentialsUpdated = true;
|
||||
}
|
||||
|
||||
if (nodeCredentials.id) {
|
||||
// Check whether the id is matching with a credential
|
||||
const credentialsForId = credentialOptions.find((optionData: ICredentialsResponse) => optionData.id === nodeCredentials.id);
|
||||
if (credentialsForId) {
|
||||
if (credentialsForId.name !== nodeCredentials.name) {
|
||||
node.credentials![nodeCredentialType].name = credentialsForId.name;
|
||||
this.credentialsUpdated = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No match for id found or old credentials type used
|
||||
node.credentials![nodeCredentialType] = nodeCredentials;
|
||||
|
||||
// check if only one option with the name would exist
|
||||
const credentialsForName = credentialOptions.filter((optionData: ICredentialsResponse) => optionData.name === nodeCredentials.name);
|
||||
|
||||
// only one option exists for the name, take it
|
||||
if (credentialsForName.length === 1) {
|
||||
node.credentials![nodeCredentialType].id = credentialsForName[0].id;
|
||||
this.credentialsUpdated = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
async addNodes (nodes: INodeUi[], connections?: IConnections) {
|
||||
if (!nodes || !nodes.length) {
|
||||
return;
|
||||
@@ -1920,6 +1966,9 @@ export default mixins(
|
||||
}
|
||||
}
|
||||
|
||||
// check and match credentials, apply new format if old is used
|
||||
this.matchCredentials(node);
|
||||
|
||||
foundNodeIssues = this.getNodeIssues(nodeType, node);
|
||||
|
||||
if (foundNodeIssues !== null) {
|
||||
|
||||
Reference in New Issue
Block a user