From 7e94eb88c35d5aeaab89178c2660970cf7f39ff7 Mon Sep 17 00:00:00 2001 From: Andreas Fitzek Date: Tue, 16 Dec 2025 16:08:05 +0100 Subject: [PATCH] chore(core): Update workflow status endpoint to use absolute URL (#23285) --- .../workflow-status.controller.test.ts | 47 +++++++++++++++++-- .../workflow-status.controller.ts | 9 +++- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/modules/dynamic-credentials.ee/__tests__/workflow-status.controller.test.ts b/packages/cli/src/modules/dynamic-credentials.ee/__tests__/workflow-status.controller.test.ts index 8451d321144..06ee3ddec54 100644 --- a/packages/cli/src/modules/dynamic-credentials.ee/__tests__/workflow-status.controller.test.ts +++ b/packages/cli/src/modules/dynamic-credentials.ee/__tests__/workflow-status.controller.test.ts @@ -5,10 +5,14 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { WorkflowStatusController } from '../workflow-status.controller'; import type { CredentialResolverWorkflowService } from '../services/credential-resolver-workflow.service'; import { UnauthenticatedError } from '@/errors/response-errors/unauthenticated.error'; +import type { UrlService } from '@/services/url.service'; +import type { GlobalConfig } from '@n8n/config'; describe('WorkflowStatusController', () => { let controller: WorkflowStatusController; let mockService: jest.Mocked; + let mockUrlService: jest.Mocked; + let mockGlobalConfig: jest.Mocked; beforeEach(() => { jest.clearAllMocks(); @@ -17,7 +21,17 @@ describe('WorkflowStatusController', () => { getWorkflowStatus: jest.fn(), } as unknown as jest.Mocked; - controller = new WorkflowStatusController(mockService); + mockUrlService = { + getInstanceBaseUrl: jest.fn().mockReturnValue('https://n8n.example.com'), + } as unknown as jest.Mocked; + + mockGlobalConfig = { + endpoints: { + rest: 'rest', + }, + } as unknown as jest.Mocked; + + controller = new WorkflowStatusController(mockService, mockUrlService, mockGlobalConfig); }); describe('checkWorkflowForExecution', () => { @@ -165,7 +179,7 @@ describe('WorkflowStatusController', () => { const result = await controller.checkWorkflowForExecution(req, res); expect(result.credentials?.[0].authorizationUrl).toBe( - '/credentials/cred-1/authorize?resolverId=resolver-1', + 'https://n8n.example.com/rest/credentials/cred-1/authorize?resolverId=resolver-1', ); }); @@ -188,10 +202,34 @@ describe('WorkflowStatusController', () => { const result = await controller.checkWorkflowForExecution(req, res); expect(result.credentials?.[0].authorizationUrl).toBe( - '/credentials/cred-1/authorize?resolverId=resolver%2Fwith%2Fspecial%20chars', + 'https://n8n.example.com/rest/credentials/cred-1/authorize?resolverId=resolver%2Fwith%2Fspecial%20chars', ); }); + it('should return absolute URLs with base path from UrlService', async () => { + const req = mock({ + params: { workflowId: 'workflow-1' }, + headers: { authorization: 'Bearer token-123' }, + }); + const res = mock(); + + mockService.getWorkflowStatus.mockResolvedValue([ + { + credentialId: 'cred-1', + resolverId: 'resolver-1', + status: 'configured', + credentialType: 'oauth2Api', + }, + ]); + + const result = await controller.checkWorkflowForExecution(req, res); + + // Verify that the URL is absolute and starts with the base URL + expect(result.credentials?.[0].authorizationUrl).toMatch(/^https:\/\//); + expect(result.credentials?.[0].authorizationUrl).toContain('https://n8n.example.com'); + expect(mockUrlService.getInstanceBaseUrl).toHaveBeenCalled(); + }); + it('should map service response to DTO correctly', async () => { const req = mock({ params: { workflowId: 'workflow-1' }, @@ -218,7 +256,8 @@ describe('WorkflowStatusController', () => { credentialId: 'cred-1', credentialStatus: 'missing', credentialType: 'oauth2Api', - authorizationUrl: '/credentials/cred-1/authorize?resolverId=resolver-1', + authorizationUrl: + 'https://n8n.example.com/rest/credentials/cred-1/authorize?resolverId=resolver-1', }, ], }); diff --git a/packages/cli/src/modules/dynamic-credentials.ee/workflow-status.controller.ts b/packages/cli/src/modules/dynamic-credentials.ee/workflow-status.controller.ts index efc1ceb0e7d..02baee90b59 100644 --- a/packages/cli/src/modules/dynamic-credentials.ee/workflow-status.controller.ts +++ b/packages/cli/src/modules/dynamic-credentials.ee/workflow-status.controller.ts @@ -5,11 +5,15 @@ import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { CredentialResolverWorkflowService } from './services/credential-resolver-workflow.service'; import { WorkflowExecutionStatus } from '@n8n/api-types'; import { getBearerToken } from './utils'; +import { UrlService } from '@/services/url.service'; +import { GlobalConfig } from '@n8n/config'; @RestController('/workflows') export class WorkflowStatusController { constructor( private readonly credentialResolverWorkflowService: CredentialResolverWorkflowService, + private readonly urlService: UrlService, + private readonly globalConfig: GlobalConfig, ) {} /** @@ -37,6 +41,9 @@ export class WorkflowStatusController { const isReady = status.every((s) => s.status === 'configured'); + const basePath = this.urlService.getInstanceBaseUrl(); + const restPath = this.globalConfig.endpoints.rest; + const executionStatus: WorkflowExecutionStatus = { workflowId, readyToExecute: isReady, @@ -44,7 +51,7 @@ export class WorkflowStatusController { credentialId: s.credentialId, credentialStatus: s.status, credentialType: s.credentialType, - authorizationUrl: `/credentials/${s.credentialId}/authorize?resolverId=${encodeURIComponent(s.resolverId)}`, + authorizationUrl: `${basePath}/${restPath}/credentials/${s.credentialId}/authorize?resolverId=${encodeURIComponent(s.resolverId)}`, })), }; return executionStatus;