From 151fcf113787a01bb83e958e80de6d190dbba08a Mon Sep 17 00:00:00 2001 From: Jaakko Husso Date: Tue, 25 Nov 2025 16:47:27 +0200 Subject: [PATCH] feat(core): Use personal projects for chat hub executions (no-changelog) (#22231) --- .../chat-hub-credentials.service.test.ts | 281 +++++++++++------- .../chat-hub/chat-hub-agent.service.ts | 6 +- .../chat-hub/chat-hub-credentials.service.ts | 83 ++++-- .../src/modules/chat-hub/chat-hub.service.ts | 19 +- .../chatHub/composables/useChatCredentials.ts | 36 ++- 5 files changed, 259 insertions(+), 166 deletions(-) diff --git a/packages/cli/src/modules/chat-hub/__tests__/chat-hub-credentials.service.test.ts b/packages/cli/src/modules/chat-hub/__tests__/chat-hub-credentials.service.test.ts index 80c620dc569..60675721fae 100644 --- a/packages/cli/src/modules/chat-hub/__tests__/chat-hub-credentials.service.test.ts +++ b/packages/cli/src/modules/chat-hub/__tests__/chat-hub-credentials.service.test.ts @@ -1,20 +1,37 @@ -import type { User } from '@n8n/db'; -import { mock } from 'jest-mock-extended'; +import type { ChatHubLLMProvider } from '@n8n/api-types'; +import type { + CredentialsEntity, + Project, + ProjectRepository, + SharedWorkflowRepository, + User, +} from '@n8n/db'; import type { EntityManager } from '@n8n/typeorm'; +import { mock } from 'jest-mock-extended'; import type { INodeCredentials } from 'n8n-workflow'; -import { - ChatHubCredentialsService, - type CredentialWithProjectId, -} from '../chat-hub-credentials.service'; -import type { CredentialsFinderService } from '@/credentials/credentials-finder.service'; +import { ChatHubCredentialsService } from '../chat-hub-credentials.service'; + +import type { CredentialsService } from '@/credentials/credentials.service'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { ForbiddenError } from '@/errors/response-errors/forbidden.error'; -import type { ChatHubLLMProvider } from '@n8n/api-types'; + +const CREDENTIAL_ID = 'credential-id-123'; +const OTHER_CREDENTIAL_ID = 'other-credential-id-456'; +const PERSONAL_PROJECT_ID = 'personal-project-id'; +const OTHER_PROJECT_ID = 'other-project-id'; +const GLOBAL_PROJECT_ID = 'global-project-id'; describe('ChatHubCredentialsService', () => { - const credentialsFinderService = mock(); - const service = new ChatHubCredentialsService(credentialsFinderService); + const credentialsService = mock(); + const projectRepository = mock(); + const sharedWorkflowRepository = mock(); + + const service = new ChatHubCredentialsService( + credentialsService, + projectRepository, + sharedWorkflowRepository, + ); const mockUser = mock({ id: 'user-123' }); const mockTrx = mock(); @@ -25,49 +42,70 @@ describe('ChatHubCredentialsService', () => { describe('ensureCredentials', () => { it('should return credential when user has access and credential is found', async () => { - const mockCredential = mock({ - id: 'cred-123', + const mockCredential = mock({ + id: CREDENTIAL_ID, + projectId: PERSONAL_PROJECT_ID, name: 'OpenAI Credentials', type: 'openAiApi', - projectId: 'project-456', + scopes: ['credential:read'], + isManaged: false, + isGlobal: false, }); const credentials: INodeCredentials = { - openAiApi: { id: 'cred-123', name: 'OpenAI Credentials' }, + openAiApi: { id: CREDENTIAL_ID, name: 'OpenAI Credentials' }, }; - credentialsFinderService.findAllCredentialsForUser.mockResolvedValue([mockCredential]); + projectRepository.getPersonalProjectForUser.mockResolvedValue({ + id: PERSONAL_PROJECT_ID, + name: 'Personal Project', + } as Project); + credentialsService.getCredentialsAUserCanUseInAWorkflow.mockResolvedValue([mockCredential]); const result = await service.ensureCredentials( mockUser, - 'openai' as ChatHubLLMProvider, + 'openai' satisfies ChatHubLLMProvider, credentials, mockTrx, ); - expect(result).toEqual(mockCredential); - expect(credentialsFinderService.findAllCredentialsForUser).toHaveBeenCalledWith( - mockUser, - ['credential:read'], + expect(projectRepository.getPersonalProjectForUser).toHaveBeenCalledWith( + mockUser.id, mockTrx, - { includeGlobalCredentials: true }, ); + expect(credentialsService.getCredentialsAUserCanUseInAWorkflow).toHaveBeenCalledWith( + mockUser, + { projectId: PERSONAL_PROJECT_ID }, + ); + + expect(result).toEqual({ + id: mockCredential.id, + projectId: mockCredential.projectId, + }); }); it('should include global credentials when fetching credentials', async () => { - const mockGlobalCredential = mock({ - id: 'global-cred-123', + const mockGlobalCredential = mock({ + id: CREDENTIAL_ID, + projectId: GLOBAL_PROJECT_ID, name: 'Global OpenAI Credentials', type: 'openAiApi', + scopes: ['credential:read'], + isManaged: false, isGlobal: true, - projectId: 'project-global', }); const credentials: INodeCredentials = { - openAiApi: { id: 'global-cred-123', name: 'Global OpenAI Credentials' }, + openAiApi: { id: CREDENTIAL_ID, name: 'Global OpenAI Credentials' }, }; - credentialsFinderService.findAllCredentialsForUser.mockResolvedValue([mockGlobalCredential]); + projectRepository.getPersonalProjectForUser.mockResolvedValue({ + id: PERSONAL_PROJECT_ID, + name: 'Personal Project', + } as Project); + credentialsService.getCredentialsAUserCanUseInAWorkflow.mockResolvedValue([ + mockGlobalCredential, + ]); const result = await service.ensureCredentials( mockUser, @@ -76,20 +114,24 @@ describe('ChatHubCredentialsService', () => { mockTrx, ); - expect(result).toEqual(mockGlobalCredential); - expect(credentialsFinderService.findAllCredentialsForUser).toHaveBeenCalledWith( - mockUser, - ['credential:read'], + expect(projectRepository.getPersonalProjectForUser).toHaveBeenCalledWith( + mockUser.id, mockTrx, - { includeGlobalCredentials: true }, ); + expect(credentialsService.getCredentialsAUserCanUseInAWorkflow).toHaveBeenCalledWith( + mockUser, + { projectId: PERSONAL_PROJECT_ID }, + ); + + expect(result).toEqual({ + id: mockGlobalCredential.id, + projectId: PERSONAL_PROJECT_ID, + }); }); it('should throw BadRequestError when no credentials are provided', async () => { const credentials: INodeCredentials = {}; - credentialsFinderService.findAllCredentialsForUser.mockResolvedValue([]); - await expect( service.ensureCredentials(mockUser, 'openai' as ChatHubLLMProvider, credentials, mockTrx), ).rejects.toThrow(BadRequestError); @@ -99,32 +141,46 @@ describe('ChatHubCredentialsService', () => { }); it('should throw ForbiddenError when user does not have access to the credential', async () => { - const mockCredential = mock({ - id: 'other-cred-456', + const mockCredential = mock({ + id: OTHER_CREDENTIAL_ID, + projectId: OTHER_PROJECT_ID, name: 'Other Credentials', type: 'openAiApi', - projectId: 'project-other', + scopes: ['credential:read'], + isManaged: false, + isGlobal: false, }); const credentials: INodeCredentials = { openAiApi: { id: 'cred-123', name: 'OpenAI Credentials' }, }; - credentialsFinderService.findAllCredentialsForUser.mockResolvedValue([mockCredential]); + projectRepository.getPersonalProjectForUser.mockResolvedValue({ + id: PERSONAL_PROJECT_ID, + name: 'Personal Project', + } as Project); + credentialsService.getCredentialsAUserCanUseInAWorkflow.mockResolvedValue([mockCredential]); await expect( service.ensureCredentials(mockUser, 'openai' as ChatHubLLMProvider, credentials, mockTrx), - ).rejects.toThrow(ForbiddenError); + ).rejects.toThrow(new ForbiddenError("You don't have access to the provided credentials")); + }); + + it('should throw ForbiddenError if personal project is not found', async () => { + const credentials: INodeCredentials = { + openAiApi: { id: 'cred-123', name: 'OpenAI Credentials' }, + }; + + projectRepository.getPersonalProjectForUser.mockResolvedValue(null); + await expect( service.ensureCredentials(mockUser, 'openai' as ChatHubLLMProvider, credentials, mockTrx), - ).rejects.toThrow("You don't have access to the provided credentials"); + ).rejects.toThrow(new ForbiddenError('Missing personal project')); }); it('should handle n8n provider by returning null credential ID', async () => { const credentials: INodeCredentials = {}; - credentialsFinderService.findAllCredentialsForUser.mockResolvedValue([]); - await expect( service.ensureCredentials(mockUser, 'n8n' as ChatHubLLMProvider, credentials, mockTrx), ).rejects.toThrow(BadRequestError); @@ -133,8 +189,6 @@ describe('ChatHubCredentialsService', () => { it('should handle custom-agent provider by returning null credential ID', async () => { const credentials: INodeCredentials = {}; - credentialsFinderService.findAllCredentialsForUser.mockResolvedValue([]); - await expect( service.ensureCredentials( mockUser, @@ -144,101 +198,100 @@ describe('ChatHubCredentialsService', () => { ), ).rejects.toThrow(BadRequestError); }); + }); - it('should return first credential when credential is shared through multiple projects', async () => { - const mockCredential = mock({ - id: 'cred-123', - name: 'Shared Credentials', + describe('ensureWorkflowCredentials', () => { + it('should return credential when workflow can use the credential', async () => { + const mockCredential = mock({ + id: CREDENTIAL_ID, + projectId: PERSONAL_PROJECT_ID, + name: 'OpenAI Credentials', type: 'openAiApi', - projectId: 'project-1', + scopes: ['credential:read'], + isManaged: false, + isGlobal: false, }); const credentials: INodeCredentials = { - openAiApi: { id: 'cred-123', name: 'Shared Credentials' }, + openAiApi: { id: CREDENTIAL_ID, name: 'OpenAI Credentials' }, }; - credentialsFinderService.findAllCredentialsForUser.mockResolvedValue([mockCredential]); + sharedWorkflowRepository.getWorkflowOwningProject.mockResolvedValue({ + id: PERSONAL_PROJECT_ID, + } as Project); + credentialsService.findAllCredentialIdsForWorkflow.mockResolvedValue([ + { id: CREDENTIAL_ID }, + { id: OTHER_CREDENTIAL_ID }, + ] as CredentialsEntity[]); - const result = await service.ensureCredentials( - mockUser, - 'openai' as ChatHubLLMProvider, + const result = await service.ensureWorkflowCredentials( + 'openai' satisfies ChatHubLLMProvider, credentials, - mockTrx, + 'workflow-123', ); - expect(result).toEqual(mockCredential); - expect(result).toHaveProperty('projectId'); - }); - }); + expect(sharedWorkflowRepository.getWorkflowOwningProject).toHaveBeenCalledWith( + 'workflow-123', + ); + expect(credentialsService.findAllCredentialIdsForWorkflow).toHaveBeenCalledWith( + 'workflow-123', + ); - describe('ensureCredentialById', () => { - it('should return credential when user has access to the credential', async () => { - const mockCredential = mock({ - id: 'cred-123', - name: 'OpenAI Credentials', - type: 'openAiApi', - projectId: 'project-456', + expect(result).toEqual({ + id: mockCredential.id, + projectId: mockCredential.projectId, }); + }); - credentialsFinderService.findAllCredentialsForUser.mockResolvedValue([mockCredential]); + it('should throw BadRequestError when no credentials are provided for workflow', async () => { + const credentials: INodeCredentials = {}; - const result = await service.ensureCredentialById(mockUser, 'cred-123', mockTrx); - - expect(result).toEqual(mockCredential); - expect(credentialsFinderService.findAllCredentialsForUser).toHaveBeenCalledWith( - mockUser, - ['credential:read'], - mockTrx, - { includeGlobalCredentials: true }, + await expect( + service.ensureWorkflowCredentials( + 'openai' as ChatHubLLMProvider, + credentials, + 'workflow-123', + ), + ).rejects.toThrow( + new BadRequestError('No credentials provided for the selected model provider'), ); }); - it('should include global credentials when fetching by ID', async () => { - const mockGlobalCredential = mock({ - id: 'global-cred-123', - name: 'Global OpenAI Credentials', - type: 'openAiApi', - isGlobal: true, - projectId: 'project-global', - }); + it('should throw ForbiddenError when workflow does not have access to the credential', async () => { + const credentials: INodeCredentials = { + openAiApi: { id: CREDENTIAL_ID, name: 'OpenAI Credentials' }, + }; - credentialsFinderService.findAllCredentialsForUser.mockResolvedValue([mockGlobalCredential]); + sharedWorkflowRepository.getWorkflowOwningProject.mockResolvedValue({ + id: PERSONAL_PROJECT_ID, + } as Project); + credentialsService.findAllCredentialIdsForWorkflow.mockResolvedValue([ + { id: OTHER_CREDENTIAL_ID }, + ] as CredentialsEntity[]); - const result = await service.ensureCredentialById(mockUser, 'global-cred-123', mockTrx); - - expect(result).toEqual(mockGlobalCredential); - expect(credentialsFinderService.findAllCredentialsForUser).toHaveBeenCalledWith( - mockUser, - ['credential:read'], - mockTrx, - { includeGlobalCredentials: true }, - ); + await expect( + service.ensureWorkflowCredentials( + 'openai' satisfies ChatHubLLMProvider, + credentials, + 'workflow-123', + ), + ).rejects.toThrow(new ForbiddenError("You don't have access to the provided credentials")); }); - it('should throw ForbiddenError when user does not have access to the credential', async () => { - const mockCredential = mock({ - id: 'other-cred-456', - name: 'Other Credentials', - type: 'openAiApi', - projectId: 'project-other', - }); + it('should throw ForbiddenError if workflow owning project is not found', async () => { + const credentials: INodeCredentials = { + openAiApi: { id: CREDENTIAL_ID, name: 'OpenAI Credentials' }, + }; - credentialsFinderService.findAllCredentialsForUser.mockResolvedValue([mockCredential]); + sharedWorkflowRepository.getWorkflowOwningProject.mockResolvedValue(undefined); - await expect(service.ensureCredentialById(mockUser, 'cred-123', mockTrx)).rejects.toThrow( - ForbiddenError, - ); - await expect(service.ensureCredentialById(mockUser, 'cred-123', mockTrx)).rejects.toThrow( - "You don't have access to the provided credentials", - ); - }); - - it('should throw ForbiddenError when credential is not found', async () => { - credentialsFinderService.findAllCredentialsForUser.mockResolvedValue([]); - - await expect(service.ensureCredentialById(mockUser, 'cred-123', mockTrx)).rejects.toThrow( - ForbiddenError, - ); + await expect( + service.ensureWorkflowCredentials( + 'openai' satisfies ChatHubLLMProvider, + credentials, + 'workflow-123', + ), + ).rejects.toThrow(new ForbiddenError('Missing owner project for the workflow')); }); }); }); diff --git a/packages/cli/src/modules/chat-hub/chat-hub-agent.service.ts b/packages/cli/src/modules/chat-hub/chat-hub-agent.service.ts index 00e0f7c0c89..8c197ae2e53 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-agent.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-agent.service.ts @@ -2,14 +2,14 @@ import { ChatModelsResponse } from '@n8n/api-types'; import { Logger } from '@n8n/backend-common'; import type { User } from '@n8n/db'; import { Service } from '@n8n/di'; +import { INode } from 'n8n-workflow'; import { v4 as uuidv4 } from 'uuid'; -import { NotFoundError } from '@/errors/response-errors/not-found.error'; - import type { ChatHubAgent } from './chat-hub-agent.entity'; import { ChatHubAgentRepository } from './chat-hub-agent.repository'; import { ChatHubCredentialsService } from './chat-hub-credentials.service'; -import { INode } from 'n8n-workflow'; + +import { NotFoundError } from '@/errors/response-errors/not-found.error'; @Service() export class ChatHubAgentService { diff --git a/packages/cli/src/modules/chat-hub/chat-hub-credentials.service.ts b/packages/cli/src/modules/chat-hub/chat-hub-credentials.service.ts index 90a4bb86279..72511e7798e 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub-credentials.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub-credentials.service.ts @@ -3,64 +3,60 @@ import { PROVIDER_CREDENTIAL_TYPE_MAP, type ChatHubConversationModel, } from '@n8n/api-types'; -import type { User, CredentialsEntity } from '@n8n/db'; +import { type User, 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 credentialsService: CredentialsService, + private readonly projectRepository: ProjectRepository, + private readonly sharedWorkflowRepository: SharedWorkflowRepository, + ) {} async ensureCredentials( user: User, provider: ChatHubLLMProvider, credentials: INodeCredentials, trx?: EntityManager, - ): Promise { - const allCredentials = await this.credentialsFinderService.findAllCredentialsForUser( - user, - ['credential:read'], - trx, - { includeGlobalCredentials: true }, - ); - + ) { const credentialId = this.pickCredentialId(provider, credentials); if (!credentialId) { throw new BadRequestError('No credentials provided for the selected model provider'); } - // 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 await this.ensureCredentialById(user, credentialId, trx); } - async ensureCredentialById( - user: User, - credentialId: string, - trx?: EntityManager, - ): Promise { - const allCredentials = await this.credentialsFinderService.findAllCredentialsForUser( + async ensureCredentialById(user: User, credentialId: string, trx?: EntityManager) { + 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, + }, ); 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, + }; } private pickCredentialId( @@ -73,4 +69,33 @@ export class ChatHubCredentialsService { return credentials[PROVIDER_CREDENTIAL_TYPE_MAP[provider]]?.id ?? null; } + + 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); + + 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, + }; + } } diff --git a/packages/cli/src/modules/chat-hub/chat-hub.service.ts b/packages/cli/src/modules/chat-hub/chat-hub.service.ts index b53bb0029c5..15f8fdb97b0 100644 --- a/packages/cli/src/modules/chat-hub/chat-hub.service.ts +++ b/packages/cli/src/modules/chat-hub/chat-hub.service.ts @@ -52,7 +52,7 @@ import { WorkflowFinderService } from '@/workflows/workflow-finder.service'; import { WorkflowService } from '@/workflows/workflow.service'; 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 { ChatHubAttachmentService } from './chat-hub.attachment.service'; @@ -1688,7 +1688,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); @@ -1714,18 +1714,18 @@ 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 }, + { includeTags: false, includeParentFolder: false, em: trx }, ); if (!workflowEntity) { @@ -1762,11 +1762,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 = { @@ -1807,7 +1806,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) { diff --git a/packages/frontend/editor-ui/src/features/ai/chatHub/composables/useChatCredentials.ts b/packages/frontend/editor-ui/src/features/ai/chatHub/composables/useChatCredentials.ts index 19a5d55eb60..6defd6cdc0b 100644 --- a/packages/frontend/editor-ui/src/features/ai/chatHub/composables/useChatCredentials.ts +++ b/packages/frontend/editor-ui/src/features/ai/chatHub/composables/useChatCredentials.ts @@ -1,6 +1,8 @@ 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, @@ -8,7 +10,7 @@ import { type ChatHubProvider, } from '@n8n/api-types'; import { useLocalStorage } from '@vueuse/core'; -import { computed, onMounted, ref } from 'vue'; +import { computed, ref, watch } from 'vue'; import { isLlmProvider } from '../chat.utils'; /** @@ -18,6 +20,7 @@ export function useChatCredentials(userId: string) { const isInitialized = ref(false); const credentialsStore = useCredentialsStore(); const settingsStore = useSettingsStore(); + const projectStore = useProjectsStore(); const selectedCredentials = useLocalStorage( LOCAL_STORAGE_CHAT_HUB_CREDENTIALS(userId), @@ -60,8 +63,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]; @@ -90,13 +92,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 }; }