mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-01 01:07:04 +02:00
feat(core): Use personal projects for chat hub executions (no-changelog) (#22231)
This commit is contained in:
parent
be598062fb
commit
151fcf1137
|
|
@ -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<CredentialsFinderService>();
|
||||
const service = new ChatHubCredentialsService(credentialsFinderService);
|
||||
const credentialsService = mock<CredentialsService>();
|
||||
const projectRepository = mock<ProjectRepository>();
|
||||
const sharedWorkflowRepository = mock<SharedWorkflowRepository>();
|
||||
|
||||
const service = new ChatHubCredentialsService(
|
||||
credentialsService,
|
||||
projectRepository,
|
||||
sharedWorkflowRepository,
|
||||
);
|
||||
|
||||
const mockUser = mock<User>({ id: 'user-123' });
|
||||
const mockTrx = mock<EntityManager>();
|
||||
|
|
@ -25,49 +42,70 @@ describe('ChatHubCredentialsService', () => {
|
|||
|
||||
describe('ensureCredentials', () => {
|
||||
it('should return credential when user has access and credential is found', async () => {
|
||||
const mockCredential = mock<CredentialWithProjectId>({
|
||||
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<CredentialWithProjectId>({
|
||||
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<CredentialWithProjectId>({
|
||||
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<CredentialWithProjectId>({
|
||||
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<CredentialWithProjectId>({
|
||||
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<CredentialWithProjectId>({
|
||||
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<CredentialWithProjectId>({
|
||||
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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<CredentialWithProjectId> {
|
||||
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<CredentialWithProjectId> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<CredentialsMap>(
|
||||
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 };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user