mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-16 09:34:07 -08:00
593 lines
18 KiB
Vue
593 lines
18 KiB
Vue
<template>
|
|
<div @keydown.stop class="credentials-input-wrapper">
|
|
<el-row>
|
|
<el-col :span="6" class="headline-regular">
|
|
Credentials Name:
|
|
<el-tooltip class="credentials-info" placement="top" effect="light">
|
|
<div slot="content" v-html="helpTexts.credentialsName"></div>
|
|
<font-awesome-icon icon="question-circle" />
|
|
</el-tooltip>
|
|
</el-col>
|
|
<el-col :span="18">
|
|
<el-input size="small" type="text" v-model="name"></el-input>
|
|
</el-col>
|
|
</el-row>
|
|
<br />
|
|
<div class="headline" v-if="credentialProperties.length">
|
|
Credential Data:
|
|
<el-tooltip class="credentials-info" placement="top" effect="light">
|
|
<div slot="content" v-html="helpTexts.credentialsData"></div>
|
|
<font-awesome-icon icon="question-circle" />
|
|
</el-tooltip>
|
|
</div>
|
|
<div v-for="parameter in credentialProperties" :key="parameter.name">
|
|
<el-row class="parameter-wrapper">
|
|
<el-col :span="6" class="parameter-name">
|
|
{{parameter.displayName}}:
|
|
<el-tooltip placement="top" class="parameter-info" v-if="parameter.description" effect="light">
|
|
<div slot="content" v-html="parameter.description"></div>
|
|
<font-awesome-icon icon="question-circle"/>
|
|
</el-tooltip>
|
|
</el-col>
|
|
<el-col :span="18">
|
|
<parameter-input :parameter="parameter" :value="propertyValue[parameter.name]" :path="parameter.name" :isCredential="true" @valueChanged="valueChanged" />
|
|
</el-col>
|
|
</el-row>
|
|
</div>
|
|
|
|
<el-row v-if="isOAuthType" class="oauth-information">
|
|
<el-col :span="6" class="headline">
|
|
OAuth
|
|
</el-col>
|
|
<el-col :span="18">
|
|
<span v-if="requiredPropertiesFilled === false">
|
|
<el-button title="Connect OAuth Credentials" circle :disabled="true">
|
|
<font-awesome-icon icon="redo" />
|
|
</el-button>
|
|
Enter all required properties
|
|
</span>
|
|
<span v-else-if="isOAuthConnected === true">
|
|
<el-button title="Reconnect OAuth Credentials" @click.stop="oAuthCredentialAuthorize()" circle>
|
|
<font-awesome-icon icon="redo" />
|
|
</el-button>
|
|
Connected
|
|
</span>
|
|
<span v-else>
|
|
<el-button title="Connect OAuth Credentials" @click.stop="oAuthCredentialAuthorize()" circle>
|
|
<font-awesome-icon icon="sign-in-alt" />
|
|
</el-button>
|
|
Not connected
|
|
</span>
|
|
|
|
<div v-if="credentialProperties.length">
|
|
<div class="clickable oauth-callback-headline" :class="{expanded: !isMinimized}" @click="isMinimized=!isMinimized" :title="isMinimized ? 'Click to display Webhook URLs' : 'Click to hide Webhook URLs'">
|
|
<font-awesome-icon icon="angle-up" class="minimize-button minimize-icon" />
|
|
OAuth Callback URL
|
|
</div>
|
|
<el-tooltip v-if="!isMinimized" class="item" effect="light" content="Click to copy Callback URL" placement="right">
|
|
<div class="callback-url left-ellipsis clickable" @click="copyCallbackUrl">
|
|
{{oAuthCallbackUrl}}
|
|
</div>
|
|
</el-tooltip>
|
|
</div>
|
|
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-row class="nodes-access-wrapper">
|
|
<el-col :span="6" class="headline">
|
|
Nodes with access:
|
|
<el-tooltip class="credentials-info" placement="top" effect="light">
|
|
<div slot="content" v-html="helpTexts.nodesWithAccess"></div>
|
|
<font-awesome-icon icon="question-circle" />
|
|
</el-tooltip>
|
|
</el-col>
|
|
<el-col :span="18">
|
|
<el-transfer
|
|
:titles="['No Access', 'Access ']"
|
|
v-model="nodesAccess"
|
|
:data="allNodesRequestingAccess">
|
|
</el-transfer>
|
|
|
|
<div v-if="nodesAccess.length === 0" class="no-nodes-access">
|
|
<strong>
|
|
Important
|
|
</strong><br />
|
|
Add at least one node which has access to the credentials!
|
|
</div>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<div class="action-buttons">
|
|
<el-button type="success" @click="updateCredentials(true)" v-if="credentialDataDynamic">
|
|
Save
|
|
</el-button>
|
|
<el-button type="success" @click="createCredentials(true)" v-else>
|
|
Create
|
|
</el-button>
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import Vue from 'vue';
|
|
|
|
import { copyPaste } from '@/components/mixins/copyPaste';
|
|
import { restApi } from '@/components/mixins/restApi';
|
|
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
|
import { showMessage } from '@/components/mixins/showMessage';
|
|
|
|
import {
|
|
ICredentialsDecryptedResponse,
|
|
ICredentialsResponse,
|
|
IUpdateInformation,
|
|
} from '@/Interface';
|
|
import {
|
|
CredentialInformation,
|
|
ICredentialDataDecryptedObject,
|
|
ICredentialsDecrypted,
|
|
ICredentialType,
|
|
ICredentialNodeAccess,
|
|
INodeCredentialDescription,
|
|
INodeParameters,
|
|
INodeProperties,
|
|
INodeTypeDescription,
|
|
NodeHelpers,
|
|
} from 'n8n-workflow';
|
|
|
|
import ParameterInput from '@/components/ParameterInput.vue';
|
|
|
|
import mixins from 'vue-typed-mixins';
|
|
|
|
export default mixins(
|
|
copyPaste,
|
|
nodeHelpers,
|
|
restApi,
|
|
showMessage,
|
|
).extend({
|
|
name: 'CredentialsInput',
|
|
props: [
|
|
'credentialTypeData', // ICredentialType
|
|
'credentialData', // ICredentialsDecryptedResponse
|
|
'nodesInit', // {
|
|
// type: Array,
|
|
// default: () => { [] },
|
|
// }
|
|
],
|
|
components: {
|
|
ParameterInput,
|
|
},
|
|
data () {
|
|
return {
|
|
isMinimized: true,
|
|
helpTexts: {
|
|
credentialsData: 'The credentials to set.',
|
|
credentialsName: 'A recognizable label for the credentials. Descriptive names work <br />best here, so you can easily select it from a list later.',
|
|
nodesWithAccess: 'Nodes with access to these credentials.',
|
|
},
|
|
credentialDataTemp: null as ICredentialsDecryptedResponse | null,
|
|
nodesAccess: [] as string[],
|
|
name: '',
|
|
propertyValue: {} as ICredentialDataDecryptedObject,
|
|
};
|
|
},
|
|
computed: {
|
|
allNodesRequestingAccess (): Array<{key: string, label: string}> {
|
|
const returnNodeTypes: string[] = [];
|
|
|
|
const nodeTypes: INodeTypeDescription[] = this.$store.getters.allNodeTypes;
|
|
|
|
let nodeType: INodeTypeDescription;
|
|
let credentialTypeDescription: INodeCredentialDescription;
|
|
|
|
// Find the node types which need the credentials
|
|
for (nodeType of nodeTypes) {
|
|
if (!nodeType.credentials) {
|
|
continue;
|
|
}
|
|
|
|
for (credentialTypeDescription of nodeType.credentials) {
|
|
if (credentialTypeDescription.name === (this.credentialTypeData as ICredentialType).name && !returnNodeTypes.includes(credentialTypeDescription.name)) {
|
|
returnNodeTypes.push(nodeType.name);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return the data in the correct format el-transfer expects
|
|
return returnNodeTypes.map((nodeTypeName: string) => {
|
|
return {
|
|
key: nodeTypeName,
|
|
label: this.$store.getters.nodeType(nodeTypeName).displayName as string,
|
|
};
|
|
});
|
|
},
|
|
credentialProperties (): INodeProperties[] {
|
|
return this.credentialTypeData.properties.filter((propertyData: INodeProperties) => {
|
|
if (!this.displayCredentialParameter(propertyData)) {
|
|
return false;
|
|
}
|
|
return !this.credentialTypeData.__overwrittenProperties || !this.credentialTypeData.__overwrittenProperties.includes(propertyData.name);
|
|
});
|
|
},
|
|
credentialDataDynamic (): ICredentialsDecryptedResponse | null {
|
|
if (this.credentialData) {
|
|
return this.credentialData;
|
|
}
|
|
|
|
return this.credentialDataTemp;
|
|
},
|
|
isOAuthType (): boolean {
|
|
if (['oAuth1Api', 'oAuth2Api'].includes(this.credentialTypeData.name)) {
|
|
return true;
|
|
}
|
|
const types = this.parentTypes(this.credentialTypeData.name);
|
|
return types.includes('oAuth1Api') || types.includes('oAuth2Api');
|
|
},
|
|
isOAuthConnected (): boolean {
|
|
if (this.isOAuthType === false) {
|
|
return false;
|
|
}
|
|
|
|
return this.credentialDataDynamic !== null && !!this.credentialDataDynamic.data!.oauthTokenData;
|
|
},
|
|
oAuthCallbackUrl (): string {
|
|
const types = this.parentTypes(this.credentialTypeData.name);
|
|
const oauthType = (this.credentialTypeData.name === 'oAuth2Api' || types.includes('oAuth2Api')) ? 'oauth2' : 'oauth1';
|
|
return this.$store.getters.oauthCallbackUrls[oauthType];
|
|
},
|
|
requiredPropertiesFilled (): boolean {
|
|
for (const property of this.credentialProperties) {
|
|
if (property.required !== true) {
|
|
continue;
|
|
}
|
|
|
|
if (!this.propertyValue[property.name]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
},
|
|
methods: {
|
|
copyCallbackUrl (): void {
|
|
this.copyToClipboard(this.oAuthCallbackUrl);
|
|
|
|
this.$showMessage({
|
|
title: 'Copied',
|
|
message: `Callback URL was successfully copied!`,
|
|
type: 'success',
|
|
});
|
|
},
|
|
parentTypes (name: string): string[] {
|
|
const credentialType = this.$store.getters.credentialType(name);
|
|
|
|
if (credentialType === undefined || credentialType.extends === undefined) {
|
|
return [];
|
|
}
|
|
|
|
const types: string[] = [];
|
|
for (const typeName of credentialType.extends) {
|
|
types.push(typeName);
|
|
types.push.apply(types, this.parentTypes(typeName));
|
|
}
|
|
|
|
return types;
|
|
},
|
|
valueChanged (parameterData: IUpdateInformation) {
|
|
const name = parameterData.name.split('.').pop() as string;
|
|
// For a currently for me unknown reason can In not simply just
|
|
// set the value and it has to be this way.
|
|
const tempValue = JSON.parse(JSON.stringify(this.propertyValue));
|
|
tempValue[name] = parameterData.value;
|
|
Vue.set(this, 'propertyValue', tempValue);
|
|
},
|
|
displayCredentialParameter (parameter: INodeProperties): boolean {
|
|
if (parameter.type === 'hidden') {
|
|
return false;
|
|
}
|
|
|
|
if (parameter.displayOptions === undefined) {
|
|
// If it is not defined no need to do a proper check
|
|
return true;
|
|
}
|
|
|
|
return this.displayParameter(this.propertyValue as INodeParameters, parameter, '');
|
|
},
|
|
async createCredentials (closeDialog: boolean): Promise<ICredentialsResponse | null> {
|
|
const nodesAccess = this.nodesAccess.map((nodeType) => {
|
|
return {
|
|
nodeType,
|
|
};
|
|
});
|
|
|
|
const newCredentials = {
|
|
name: this.name,
|
|
type: (this.credentialTypeData as ICredentialType).name,
|
|
nodesAccess,
|
|
// Save only the none default data
|
|
data: NodeHelpers.getNodeParameters(this.credentialTypeData.properties as INodeProperties[], this.propertyValue as INodeParameters, false, false),
|
|
} as ICredentialsDecrypted;
|
|
|
|
let result;
|
|
try {
|
|
result = await this.restApi().createNewCredentials(newCredentials);
|
|
} catch (error) {
|
|
this.$showError(error, 'Problem Creating Credentials', 'There was a problem creating the credentials:');
|
|
return null;
|
|
}
|
|
|
|
// Add also to local store
|
|
this.$store.commit('addCredentials', result);
|
|
|
|
this.$emit('credentialsCreated', {data: result, options: { closeDialog }});
|
|
|
|
return result;
|
|
},
|
|
async oAuthCredentialAuthorize () {
|
|
let url;
|
|
|
|
let credentialData = this.credentialDataDynamic;
|
|
let newCredentials = false;
|
|
if (!credentialData) {
|
|
// Credentials did not get created yet. So create first before
|
|
// doing oauth authorize
|
|
credentialData = await this.createCredentials(false) as ICredentialsDecryptedResponse;
|
|
newCredentials = true;
|
|
if (credentialData === null) {
|
|
return;
|
|
}
|
|
|
|
// Set the internal data directly so that even if it fails it displays a "Save" instead
|
|
// of the "Create" button. If that would not be done, people could not retry after a
|
|
// connect issue as it woult try to create credentials again which would fail as they
|
|
// exist already.
|
|
Vue.set(this, 'credentialDataTemp', credentialData);
|
|
} else {
|
|
// Exists already but got maybe changed. So save first
|
|
credentialData = await this.updateCredentials(false) as ICredentialsDecryptedResponse;
|
|
if (credentialData === null) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const types = this.parentTypes(this.credentialTypeData.name);
|
|
|
|
try {
|
|
if (this.credentialTypeData.name === 'oAuth2Api' || types.includes('oAuth2Api')) {
|
|
url = await this.restApi().oAuth2CredentialAuthorize(credentialData as ICredentialsResponse) as string;
|
|
} else if (this.credentialTypeData.name === 'oAuth1Api' || types.includes('oAuth1Api')) {
|
|
url = await this.restApi().oAuth1CredentialAuthorize(credentialData as ICredentialsResponse) as string;
|
|
}
|
|
} catch (error) {
|
|
this.$showError(error, 'OAuth Authorization Error', 'Error generating authorization URL:');
|
|
return;
|
|
}
|
|
|
|
const params = `scrollbars=no,resizable=yes,status=no,titlebar=noe,location=no,toolbar=no,menubar=no,width=500,height=700`;
|
|
const oauthPopup = window.open(url, 'OAuth2 Authorization', params);
|
|
|
|
const receiveMessage = (event: MessageEvent) => {
|
|
// // TODO: Add check that it came from n8n
|
|
// if (event.origin !== 'http://example.org:8080') {
|
|
// return;
|
|
// }
|
|
|
|
if (event.data === 'success') {
|
|
|
|
// Set some kind of data that status changes.
|
|
// As data does not get displayed directly it does not matter what data.
|
|
if (this.credentialData === null) {
|
|
// Are new credentials so did not get send via "credentialData"
|
|
Vue.set(this, 'credentialDataTemp', credentialData);
|
|
Vue.set(this.credentialDataTemp!.data!, 'oauthTokenData', {});
|
|
} else {
|
|
// Credentials did already exist so can be set directly
|
|
Vue.set(this.credentialData.data, 'oauthTokenData', {});
|
|
}
|
|
|
|
// Save that OAuth got authorized locally
|
|
this.$store.commit('updateCredentials', this.credentialDataDynamic);
|
|
|
|
// Close the window
|
|
if (oauthPopup) {
|
|
oauthPopup.close();
|
|
}
|
|
|
|
if (newCredentials === true) {
|
|
this.$emit('credentialsCreated', {data: credentialData, options: { closeDialog: false }});
|
|
}
|
|
|
|
this.$showMessage({
|
|
title: 'Connected',
|
|
message: 'Connected successfully!',
|
|
type: 'success',
|
|
});
|
|
|
|
// Make sure that the event gets removed again
|
|
window.removeEventListener('message', receiveMessage, false);
|
|
}
|
|
|
|
};
|
|
|
|
window.addEventListener('message', receiveMessage, false);
|
|
},
|
|
async updateCredentials (closeDialog: boolean): Promise<ICredentialsResponse | null> {
|
|
const nodesAccess: ICredentialNodeAccess[] = [];
|
|
const addedNodeTypes: string[] = [];
|
|
|
|
// Add Node-type which already had access to keep the original added date
|
|
let nodeAccessData: ICredentialNodeAccess;
|
|
for (nodeAccessData of (this.credentialDataDynamic as ICredentialsDecryptedResponse).nodesAccess) {
|
|
if (this.nodesAccess.includes((nodeAccessData.nodeType))) {
|
|
nodesAccess.push(nodeAccessData);
|
|
addedNodeTypes.push(nodeAccessData.nodeType);
|
|
}
|
|
}
|
|
|
|
// Add Node-type which did not have access before
|
|
for (const nodeType of this.nodesAccess) {
|
|
if (!addedNodeTypes.includes(nodeType)) {
|
|
nodesAccess.push({
|
|
nodeType,
|
|
});
|
|
}
|
|
}
|
|
|
|
const newCredentials = {
|
|
name: this.name,
|
|
type: (this.credentialTypeData as ICredentialType).name,
|
|
nodesAccess,
|
|
// Save only the none default data
|
|
data: NodeHelpers.getNodeParameters(this.credentialTypeData.properties as INodeProperties[], this.propertyValue as INodeParameters, false, false),
|
|
} as ICredentialsDecrypted;
|
|
|
|
let result;
|
|
try {
|
|
result = await this.restApi().updateCredentials((this.credentialDataDynamic as ICredentialsDecryptedResponse).id as string, newCredentials);
|
|
} catch (error) {
|
|
this.$showError(error, 'Problem Updating Credentials', 'There was a problem updating the credentials:');
|
|
return null;
|
|
}
|
|
|
|
// Update also in local store
|
|
this.$store.commit('updateCredentials', result);
|
|
|
|
// Now that the credentials changed check if any nodes use credentials
|
|
// which have now a different name
|
|
this.updateNodesCredentialsIssues();
|
|
|
|
this.$emit('credentialsUpdated', {data: result, options: { closeDialog }});
|
|
|
|
return result;
|
|
},
|
|
init () {
|
|
if (this.credentialData) {
|
|
// Initialize with the given data
|
|
this.name = (this.credentialData as ICredentialsDecryptedResponse).name;
|
|
this.propertyValue = (this.credentialData as ICredentialsDecryptedResponse).data as ICredentialDataDecryptedObject;
|
|
const nodesAccess = (this.credentialData as ICredentialsDecryptedResponse).nodesAccess.map((nodeAccess) => {
|
|
return nodeAccess.nodeType;
|
|
});
|
|
|
|
Vue.set(this, 'nodesAccess', nodesAccess);
|
|
} else {
|
|
// No data supplied so init empty
|
|
this.name = '';
|
|
this.propertyValue = {} as ICredentialDataDecryptedObject;
|
|
const nodesAccess = [] as string[];
|
|
nodesAccess.push.apply(nodesAccess, this.nodesInit);
|
|
|
|
Vue.set(this, 'nodesAccess', nodesAccess);
|
|
}
|
|
|
|
// Set default values
|
|
for (const property of (this.credentialTypeData as ICredentialType).properties) {
|
|
if (!this.propertyValue.hasOwnProperty(property.name)) {
|
|
this.propertyValue[property.name] = property.default as CredentialInformation;
|
|
}
|
|
}
|
|
},
|
|
},
|
|
watch: {
|
|
credentialData () {
|
|
this.init();
|
|
},
|
|
credentialTypeData () {
|
|
this.init();
|
|
},
|
|
},
|
|
mounted () {
|
|
this.init();
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
|
|
.credentials-input-wrapper {
|
|
.action-buttons {
|
|
margin-top: 2em;
|
|
text-align: right;
|
|
}
|
|
|
|
.headline {
|
|
font-weight: 600;
|
|
color: $--color-primary;
|
|
margin-bottom: 1em;
|
|
}
|
|
|
|
.nodes-access-wrapper {
|
|
margin-top: 1em;
|
|
}
|
|
|
|
.no-nodes-access {
|
|
margin: 1em 0;
|
|
color: $--color-primary;
|
|
line-height: 1.75em;
|
|
}
|
|
|
|
.oauth-information {
|
|
line-height: 2.5em;
|
|
margin: 2em 0;
|
|
}
|
|
|
|
.parameter-wrapper {
|
|
line-height: 3em;
|
|
|
|
.parameter-name {
|
|
position: relative;
|
|
|
|
&:hover {
|
|
.parameter-info {
|
|
display: inline;
|
|
}
|
|
}
|
|
|
|
.parameter-info {
|
|
display: none;
|
|
}
|
|
}
|
|
}
|
|
|
|
.credentials-info {
|
|
display: none;
|
|
}
|
|
|
|
.callback-url {
|
|
position: relative;
|
|
top: 0;
|
|
width: 100%;
|
|
font-size: 0.9em;
|
|
white-space: normal;
|
|
overflow: visible;
|
|
text-overflow: initial;
|
|
color: #404040;
|
|
text-align: left;
|
|
direction: ltr;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.headline:hover,
|
|
.headline-regular:hover {
|
|
.credentials-info {
|
|
display: inline;
|
|
}
|
|
}
|
|
|
|
.expanded .minimize-button {
|
|
-webkit-transform: rotate(180deg);
|
|
-moz-transform: rotate(180deg);
|
|
-o-transform: rotate(180deg);
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.oauth-callback-headline {
|
|
padding-top: 1em;
|
|
font-weight: 500;
|
|
}
|
|
}
|
|
|
|
</style>
|