Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 53 additions & 8 deletions packages/cli/src/modules/chat-hub/chat-hub-credentials.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,90 @@ import {
PROVIDER_CREDENTIAL_TYPE_MAP,
type ChatHubConversationModel,
} from '@n8n/api-types';
import type { User, CredentialsEntity } from '@n8n/db';
import { type User, type CredentialsEntity, ProjectRepository } from '@n8n/db';
import { SharedWorkflowRepository } from '@n8n/db';
import { Service } from '@n8n/di';
import type { EntityManager } from '@n8n/typeorm';
import type { INodeCredentials } from 'n8n-workflow';

import { CredentialsFinderService } from '@/credentials/credentials-finder.service';
import { CredentialsService } from '@/credentials/credentials.service';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';

export type CredentialWithProjectId = CredentialsEntity & { projectId: string };

@Service()
export class ChatHubCredentialsService {
constructor(private readonly credentialsFinderService: CredentialsFinderService) {}
constructor(
private readonly credentialsFinderService: CredentialsFinderService,
private readonly credentialsService: CredentialsService,
private readonly projectRepository: ProjectRepository,
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
) {}

async ensureCredentials(
user: User,
provider: ChatHubLLMProvider,
credentials: INodeCredentials,
trx?: EntityManager,
): Promise<CredentialWithProjectId> {
const allCredentials = await this.credentialsFinderService.findAllCredentialsForUser(
) {
const credentialId = this.pickCredentialId(provider, credentials);
if (!credentialId) {
throw new BadRequestError('No credentials provided for the selected model provider');
}

const project = await this.projectRepository.getPersonalProjectForUser(user.id, trx);
if (!project) {
throw new ForbiddenError('Missing personal project');
}

const allCredentials = await this.credentialsService.getCredentialsAUserCanUseInAWorkflow(
user,
['credential:read'],
trx,
{ includeGlobalCredentials: true },
{
projectId: project.id,
},
);

// If credential is shared through multiple projects just pick the first one.
const credential = allCredentials.find((c) => c.id === credentialId);
if (!credential) {
throw new ForbiddenError("You don't have access to the provided credentials");
}
return {
id: credential.id,
projectId: project.id,
};
}

async ensureWorkflowCredentials(
provider: ChatHubLLMProvider,
credentials: INodeCredentials,
workflowId: string,
) {
const credentialId = this.pickCredentialId(provider, credentials);
if (!credentialId) {
throw new BadRequestError('No credentials provided for the selected model provider');
}

const project = await this.sharedWorkflowRepository.getWorkflowOwningProject(workflowId);
if (!project) {
throw new ForbiddenError('Missing owner project for the workflow');
}

const allCredentials =
await this.credentialsService.findAllCredentialIdsForWorkflow(workflowId);

// If credential is shared through multiple projects just pick the first one.
const credential = allCredentials.find((c) => c.id === credentialId);
if (!credential) {
throw new ForbiddenError("You don't have access to the provided credentials");
}
return credential as CredentialWithProjectId;

return {
id: credential.id,
projectId: project.id,
};
}

async ensureCredentialById(
Expand Down
17 changes: 8 additions & 9 deletions packages/cli/src/modules/chat-hub/chat-hub.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {
} from 'n8n-workflow';

import { ChatHubAgentService } from './chat-hub-agent.service';
import { ChatHubCredentialsService, CredentialWithProjectId } from './chat-hub-credentials.service';
import { ChatHubCredentialsService } from './chat-hub-credentials.service';
import type { ChatHubMessage } from './chat-hub-message.entity';
import { ChatHubWorkflowService } from './chat-hub-workflow.service';
import { JSONL_STREAM_HEADERS, NODE_NAMES, PROVIDER_NODE_TYPE_MAP } from './chat-hub.constants';
Expand Down Expand Up @@ -1687,7 +1687,7 @@ export class ChatHubService {
): Promise<{
resolvedCredentials: INodeCredentials;
resolvedModel: ChatHubConversationModel;
credential: CredentialWithProjectId;
credential: { id: string; projectId: string };
}> {
if (model.provider === 'n8n') {
return await this.resolveFromN8nWorkflow(user, model, trx);
Expand All @@ -1713,15 +1713,15 @@ export class ChatHubService {

private async resolveFromN8nWorkflow(
user: User,
model: ChatHubN8nModel,
{ workflowId }: ChatHubN8nModel,
trx: EntityManager,
): Promise<{
resolvedCredentials: INodeCredentials;
resolvedModel: ChatHubConversationModel;
credential: CredentialWithProjectId;
credential: { id: string; projectId: string };
}> {
const workflowEntity = await this.workflowFinderService.findWorkflowForUser(
model.workflowId,
workflowId,
user,
['workflow:read'],
{ includeTags: false, includeParentFolder: false },
Expand Down Expand Up @@ -1761,11 +1761,10 @@ export class ChatHubService {
);
}

const credential = await this.chatHubCredentialsService.ensureCredentials(
user,
const credential = await this.chatHubCredentialsService.ensureWorkflowCredentials(
modelNode.provider,
llmCredentials,
trx,
workflowId,
);

const resolvedModel: ChatHubConversationModel = {
Expand Down Expand Up @@ -1806,7 +1805,7 @@ export class ChatHubService {
): Promise<{
resolvedCredentials: INodeCredentials;
resolvedModel: ChatHubConversationModel;
credential: CredentialWithProjectId;
credential: { id: string; projectId: string };
}> {
const agent = await this.chatHubAgentService.getAgentById(model.agentId, user.id);
if (!agent) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { LOCAL_STORAGE_CHAT_HUB_CREDENTIALS } from '@/app/constants';
import { useSettingsStore } from '@/app/stores/settings.store';
import { hasPermission } from '@/app/utils/rbac/permissions';
import { credentialsMapSchema, type CredentialsMap } from '@/features/ai/chatHub/chat.types';
import { useProjectsStore } from '@/features/collaboration/projects/projects.store';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import {
chatHubProviderSchema,
PROVIDER_CREDENTIAL_TYPE_MAP,
type ChatHubProvider,
} from '@n8n/api-types';
import { useLocalStorage } from '@vueuse/core';
import { computed, onMounted, ref } from 'vue';
import { computed, ref, watch } from 'vue';

/**
* Composable for managing chat credentials including auto-selection and user selection.
Expand All @@ -17,6 +19,7 @@ export function useChatCredentials(userId: string) {
const isInitialized = ref(false);
const credentialsStore = useCredentialsStore();
const settingsStore = useSettingsStore();
const projectStore = useProjectsStore();

const selectedCredentials = useLocalStorage<CredentialsMap>(
LOCAL_STORAGE_CHAT_HUB_CREDENTIALS(userId),
Expand Down Expand Up @@ -55,8 +58,7 @@ export function useChatCredentials(userId: string) {

// Use default credential from settings if available to the user
if (
settings &&
settings.credentialId &&
settings?.credentialId &&
availableCredentials.some((c) => c.id === settings.credentialId)
) {
return [provider, settings.credentialId];
Expand Down Expand Up @@ -85,13 +87,27 @@ export function useChatCredentials(userId: string) {
selectedCredentials.value = { ...selectedCredentials.value, [provider]: id };
}

onMounted(async () => {
await Promise.all([
credentialsStore.fetchCredentialTypes(false),
credentialsStore.fetchAllCredentials(),
]);
isInitialized.value = true;
});
watch(
() => projectStore.personalProject,
async (personalProject) => {
if (personalProject) {
const hasGlobalCredentialRead = hasPermission(['rbac'], {
rbac: { scope: 'credential:read' },
});

await Promise.all([
credentialsStore.fetchCredentialTypes(false),
// For non-owner users only fetch credentials from personal project.
hasGlobalCredentialRead
? credentialsStore.fetchAllCredentials()
: credentialsStore.fetchAllCredentials(personalProject.id),
]);

isInitialized.value = true;
}
},
{ immediate: true },
);

return { credentialsByProvider, selectCredential };
}