import { INodeUi, IUsedCredential } from './../Interface'; import { createNewCredential, deleteCredential, getAllCredentials, getCredentialData, getCredentialsNewName, getCredentialTypes, oAuth1CredentialAuthorize, oAuth2CredentialAuthorize, testCredential, updateCredential, } from '@/api/credentials'; import { setCredentialSharedWith } from '@/api/credentials.ee'; import { getAppNameFromCredType, makeRestApiRequest } from '@/utils'; import { EnterpriseEditionFeature, STORES } from '@/constants'; import { ICredentialMap, ICredentialsDecryptedResponse, ICredentialsResponse, ICredentialsState, ICredentialTypeMap, } from '@/Interface'; import { i18n } from '@/plugins/i18n'; import { ICredentialsDecrypted, ICredentialType, INodeCredentialTestResult, INodeProperties, INodeTypeDescription, IUser, } from 'n8n-workflow'; import { defineStore } from 'pinia'; import Vue from 'vue'; import { useRootStore } from './n8nRootStore'; import { useNodeTypesStore } from './nodeTypes'; import { useSettingsStore } from './settings'; import { useUsersStore } from './users'; const DEFAULT_CREDENTIAL_NAME = 'Unnamed credential'; const DEFAULT_CREDENTIAL_POSTFIX = 'account'; const TYPES_WITH_DEFAULT_NAME = ['httpBasicAuth', 'oAuth2Api', 'httpDigestAuth', 'oAuth1Api']; export const useCredentialsStore = defineStore(STORES.CREDENTIALS, { state: (): ICredentialsState => ({ credentialTypes: {}, credentials: {}, }), getters: { credentialTypesById(): Record { return this.credentialTypes; }, allCredentialTypes(): ICredentialType[] { return Object.values(this.credentialTypes).sort((a, b) => a.displayName.localeCompare(b.displayName), ); }, allCredentials(): ICredentialsResponse[] { return Object.values(this.credentials).sort((a, b) => a.name.localeCompare(b.name)); }, allCredentialsByType(): { [type: string]: ICredentialsResponse[] } { const credentials = this.allCredentials; const types = this.allCredentialTypes; return types.reduce( (accu: { [type: string]: ICredentialsResponse[] }, type: ICredentialType) => { accu[type.name] = credentials.filter( (cred: ICredentialsResponse) => cred.type === type.name, ); return accu; }, {}, ); }, allUsableCredentialsForNode() { return (node: INodeUi): ICredentialsResponse[] => { let credentials: ICredentialsResponse[] = []; const nodeType = useNodeTypesStore().getNodeType(node.type, node.typeVersion); if (nodeType && nodeType.credentials) { nodeType.credentials.forEach((cred) => { credentials = credentials.concat(this.allUsableCredentialsByType[cred.name]); }); } return credentials.sort((a, b) => { const aDate = new Date(a.updatedAt); const bDate = new Date(b.updatedAt); return aDate.getTime() - bDate.getTime(); }); }; }, allUsableCredentialsByType(): { [type: string]: ICredentialsResponse[] } { const credentials = this.allCredentials; const types = this.allCredentialTypes; const usersStore = useUsersStore(); return types.reduce( (accu: { [type: string]: ICredentialsResponse[] }, type: ICredentialType) => { accu[type.name] = credentials.filter((cred: ICredentialsResponse) => { return cred.type === type.name && usersStore.isResourceAccessible(cred); }); return accu; }, {}, ); }, getCredentialTypeByName() { return (type: string): ICredentialType => this.credentialTypes[type]; }, getCredentialById() { return (id: string): ICredentialsResponse => this.credentials[id]; }, getCredentialByIdAndType() { return (id: string, type: string): ICredentialsResponse | undefined => { const credential = this.credentials[id]; return !credential || credential.type !== type ? undefined : credential; }; }, getCredentialsByType() { return (credentialType: string): ICredentialsResponse[] => { return this.allCredentialsByType[credentialType] || []; }; }, getUsableCredentialByType() { return (credentialType: string): ICredentialsResponse[] => { return this.allUsableCredentialsByType[credentialType] || []; }; }, getNodesWithAccess() { return (credentialTypeName: string) => { const nodeTypesStore = useNodeTypesStore(); const allLatestNodeTypes: INodeTypeDescription[] = nodeTypesStore.allLatestNodeTypes; return allLatestNodeTypes.filter((nodeType: INodeTypeDescription) => { if (!nodeType.credentials) { return false; } for (const credentialTypeDescription of nodeType.credentials) { if (credentialTypeDescription.name === credentialTypeName) { return true; } } return false; }); }; }, getScopesByCredentialType() { return (credentialTypeName: string) => { const credentialType = this.getCredentialTypeByName(credentialTypeName) as { properties: INodeProperties[]; }; const scopeProperty = credentialType.properties.find((p) => p.name === 'scope'); if ( !scopeProperty || !scopeProperty.default || typeof scopeProperty.default !== 'string' || scopeProperty.default === '' ) { return []; } let { default: scopeDefault } = scopeProperty; // disregard expressions for display scopeDefault = scopeDefault.replace(/^=/, '').replace(/\{\{.*\}\}/, ''); if (/ /.test(scopeDefault)) return scopeDefault.split(' '); if (/,/.test(scopeDefault)) return scopeDefault.split(','); return [scopeDefault]; }; }, getCredentialOwnerName() { return (credential: ICredentialsResponse | IUsedCredential | undefined): string => { return credential?.ownedBy?.firstName ? `${credential.ownedBy.firstName} ${credential.ownedBy.lastName} (${credential.ownedBy.email})` : i18n.baseText('credentialEdit.credentialSharing.info.sharee.fallback'); }; }, getCredentialOwnerNameById() { return (credentialId: string): string => { const credential = this.getCredentialById(credentialId); return this.getCredentialOwnerName(credential); }; }, }, actions: { setCredentialTypes(credentialTypes: ICredentialType[]): void { this.credentialTypes = credentialTypes.reduce( (accu: ICredentialTypeMap, cred: ICredentialType) => { accu[cred.name] = cred; return accu; }, {}, ); }, setCredentials(credentials: ICredentialsResponse[]): void { this.credentials = credentials.reduce((accu: ICredentialMap, cred: ICredentialsResponse) => { if (cred.id) { accu[cred.id] = cred; } return accu; }, {}); }, addCredentials(credentials: ICredentialsResponse[]): void { credentials.forEach((cred: ICredentialsResponse) => { if (cred.id) { this.credentials[cred.id] = { ...this.credentials[cred.id], ...cred }; } }); }, upsertCredential(credential: ICredentialsResponse): void { if (credential.id) { Vue.set(this.credentials, credential.id, { ...this.credentials[credential.id], ...credential, }); } }, enableOAuthCredential(credential: ICredentialsResponse): void { // enable oauth event to track change between modals }, async fetchCredentialTypes(forceFetch: boolean): Promise { if (this.allCredentialTypes.length > 0 && forceFetch !== true) { return; } const rootStore = useRootStore(); const credentialTypes = await getCredentialTypes(rootStore.getBaseUrl); this.setCredentialTypes(credentialTypes); }, async fetchAllCredentials(): Promise { const rootStore = useRootStore(); const credentials = await getAllCredentials(rootStore.getRestApiContext); this.setCredentials(credentials); return credentials; }, async getCredentialData({ id, }: { id: string; }): Promise { const rootStore = useRootStore(); return await getCredentialData(rootStore.getRestApiContext, id); }, async createNewCredential(data: ICredentialsDecrypted): Promise { const rootStore = useRootStore(); const settingsStore = useSettingsStore(); const credential = await createNewCredential(rootStore.getRestApiContext, data); if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) { this.upsertCredential(credential); if (data.ownedBy) { this.setCredentialOwnedBy({ credentialId: credential.id, ownedBy: data.ownedBy, }); const usersStore = useUsersStore(); if (data.sharedWith && data.ownedBy.id === usersStore.currentUserId) { this.setCredentialSharedWith({ credentialId: credential.id, sharedWith: data.sharedWith, }); } } } else { this.upsertCredential(credential); } return credential; }, async updateCredential(params: { data: ICredentialsDecrypted; id: string; }): Promise { const { id, data } = params; const rootStore = useRootStore(); const settingsStore = useSettingsStore(); const credential = await updateCredential(rootStore.getRestApiContext, id, data); if (settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) { this.upsertCredential(credential); if (data.ownedBy) { this.setCredentialOwnedBy({ credentialId: credential.id, ownedBy: data.ownedBy, }); const usersStore = useUsersStore(); if (data.sharedWith && data.ownedBy.id === usersStore.currentUserId) { this.setCredentialSharedWith({ credentialId: credential.id, sharedWith: data.sharedWith, }); } } } else { this.upsertCredential(credential); } return credential; }, async deleteCredential({ id }: { id: string }) { const rootStore = useRootStore(); const deleted = await deleteCredential(rootStore.getRestApiContext, id); if (deleted) { Vue.delete(this.credentials, id); } }, async oAuth2Authorize(data: ICredentialsResponse): Promise { const rootStore = useRootStore(); return oAuth2CredentialAuthorize(rootStore.getRestApiContext, data); }, async oAuth1Authorize(data: ICredentialsResponse): Promise { const rootStore = useRootStore(); return oAuth1CredentialAuthorize(rootStore.getRestApiContext, data); }, async testCredential(data: ICredentialsDecrypted): Promise { const rootStore = useRootStore(); return testCredential(rootStore.getRestApiContext, { credentials: data }); }, async getNewCredentialName(params: { credentialTypeName: string }): Promise { try { const { credentialTypeName } = params; let newName = DEFAULT_CREDENTIAL_NAME; if (!TYPES_WITH_DEFAULT_NAME.includes(credentialTypeName)) { const { displayName } = this.getCredentialTypeByName(credentialTypeName); newName = getAppNameFromCredType(displayName); newName = newName.length > 0 ? `${newName} ${DEFAULT_CREDENTIAL_POSTFIX}` : DEFAULT_CREDENTIAL_NAME; } const rootStore = useRootStore(); const res = await getCredentialsNewName(rootStore.getRestApiContext, newName); return res.name; } catch (e) { return DEFAULT_CREDENTIAL_NAME; } }, // Enterprise edition actions setCredentialOwnedBy(payload: { credentialId: string; ownedBy: Partial }) { Vue.set(this.credentials[payload.credentialId], 'ownedBy', payload.ownedBy); }, async setCredentialSharedWith(payload: { sharedWith: IUser[]; credentialId: string }) { if (useSettingsStore().isEnterpriseFeatureEnabled(EnterpriseEditionFeature.Sharing)) { await setCredentialSharedWith(useRootStore().getRestApiContext, payload.credentialId, { shareWithIds: payload.sharedWith.map((sharee) => sharee.id), }); Vue.set(this.credentials[payload.credentialId], 'sharedWith', payload.sharedWith); } }, addCredentialSharee(payload: { credentialId: string; sharee: Partial }): void { Vue.set( this.credentials[payload.credentialId], 'sharedWith', (this.credentials[payload.credentialId].sharedWith || []).concat([payload.sharee]), ); }, removeCredentialSharee(payload: { credentialId: string; sharee: Partial }): void { Vue.set( this.credentials[payload.credentialId], 'sharedWith', (this.credentials[payload.credentialId].sharedWith || []).filter( (sharee) => sharee.id !== payload.sharee.id, ), ); }, async getCredentialTranslation(credentialType: string): Promise { const rootStore = useRootStore(); return await makeRestApiRequest( rootStore.getRestApiContext, 'GET', '/credential-translation', { credentialType }, ); }, }, });