@platform-mesh/portal-server-lib 0.5.12 → 0.5.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/portal-options/pm-portal-context.service.js +1 -3
- package/dist/portal-options/pm-portal-context.service.js.map +1 -1
- package/dist/portal-options/services/kcp-k8s.service.d.ts +3 -1
- package/dist/portal-options/services/kcp-k8s.service.js +18 -4
- package/dist/portal-options/services/kcp-k8s.service.js.map +1 -1
- package/package.json +3 -3
- package/src/portal-options/pm-portal-context.service.ts +2 -8
- package/src/portal-options/services/kcp-k8s.service.spec.ts +142 -13
- package/src/portal-options/services/kcp-k8s.service.ts +25 -4
|
@@ -22,10 +22,8 @@ let PMPortalContextService = class PMPortalContextService {
|
|
|
22
22
|
return portalContext;
|
|
23
23
|
}
|
|
24
24
|
addKcpWorkspaceUrl(request, portalContext) {
|
|
25
|
-
const organization = getOrganization(request);
|
|
26
|
-
const account = request.query?.['core_platform-mesh_io_account'];
|
|
27
25
|
portalContext.kcpWorkspaceUrl =
|
|
28
|
-
this.kcpKubernetesService.getKcpWorkspacePublicUrl(
|
|
26
|
+
this.kcpKubernetesService.getKcpWorkspacePublicUrl(request);
|
|
29
27
|
}
|
|
30
28
|
processDynamicApiUrls(request, portalContext) {
|
|
31
29
|
const org = getOrganization(request);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pm-portal-context.service.js","sourceRoot":"","sources":["../../src/portal-options/pm-portal-context.service.ts"],"names":[],"mappings":";;;;;;;;;AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAG5C,OAAO,OAAO,MAAM,cAAc,CAAC;AAG5B,IAAM,sBAAsB,GAA5B,MAAM,sBAAsB;IACb;IAApB,YAAoB,oBAA0C;QAA1C,yBAAoB,GAApB,oBAAoB,CAAsB;IAAG,CAAC;IAElE,KAAK,CAAC,gBAAgB,CACpB,OAAgB,EAChB,QAAkB,EAClB,aAA4B;QAE5B,IAAI,CAAC,qBAAqB,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;QACnD,IAAI,CAAC,kBAAkB,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;QAEhD,OAAO,aAAa,CAAC;IACvB,CAAC;IAEO,kBAAkB,
|
|
1
|
+
{"version":3,"file":"pm-portal-context.service.js","sourceRoot":"","sources":["../../src/portal-options/pm-portal-context.service.ts"],"names":[],"mappings":";;;;;;;;;AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAG5C,OAAO,OAAO,MAAM,cAAc,CAAC;AAG5B,IAAM,sBAAsB,GAA5B,MAAM,sBAAsB;IACb;IAApB,YAAoB,oBAA0C;QAA1C,yBAAoB,GAApB,oBAAoB,CAAsB;IAAG,CAAC;IAElE,KAAK,CAAC,gBAAgB,CACpB,OAAgB,EAChB,QAAkB,EAClB,aAA4B;QAE5B,IAAI,CAAC,qBAAqB,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;QACnD,IAAI,CAAC,kBAAkB,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;QAEhD,OAAO,aAAa,CAAC;IACvB,CAAC;IAEO,kBAAkB,CAAC,OAAgB,EAAE,aAA4B;QACvE,aAAa,CAAC,eAAe;YAC3B,IAAI,CAAC,oBAAoB,CAAC,wBAAwB,CAAC,OAAO,CAAC,CAAC;IAChE,CAAC;IAEO,qBAAqB,CAC3B,OAAgB,EAChB,aAA4B;QAE5B,MAAM,GAAG,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;QACrC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;QACvD,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAEnE,MAAM,YAAY,GAAG;YACnB,kBAAkB,EAAE,SAAS;YAC7B,aAAa,EAAE,GAAG;SACnB,CAAC;QAEF,MAAM,mBAAmB,GAAG,CAAC,GAAY,EAAE,EAAE,CAC3C,GAAG;YACD,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,MAAM,CACjC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,EAC9C,GAAG,CACJ;YACH,CAAC,CAAC,GAAG,CAAC;QAEV,aAAa,CAAC,gBAAgB,GAAG,mBAAmB,CAClD,aAAa,CAAC,gBAAgB,CAC/B,CAAC;QACF,aAAa,CAAC,gBAAgB,GAAG,mBAAmB,CAClD,aAAa,CAAC,gBAAgB,CAC/B,CAAC;IACJ,CAAC;CACF,CAAA;AA/CY,sBAAsB;IADlC,UAAU,EAAE;qCAE+B,oBAAoB;GADnD,sBAAsB,CA+ClC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CustomObjectsApi } from '@kubernetes/client-node';
|
|
2
|
+
import type { Request } from 'express';
|
|
2
3
|
export declare class KcpKubernetesService {
|
|
3
4
|
private readonly k8sApi;
|
|
4
5
|
private readonly baseUrl;
|
|
@@ -7,5 +8,6 @@ export declare class KcpKubernetesService {
|
|
|
7
8
|
private buildWorkspacePath;
|
|
8
9
|
getKcpVirtualWorkspaceUrl(organization: string, account: string): URL;
|
|
9
10
|
getKcpWorkspaceUrl(organization: string, account: string): URL;
|
|
10
|
-
getKcpWorkspacePublicUrl(
|
|
11
|
+
getKcpWorkspacePublicUrl(request: Request): string;
|
|
12
|
+
private getAppPort;
|
|
11
13
|
}
|
|
@@ -7,13 +7,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
8
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
9
|
};
|
|
10
|
+
import { getOrganization } from '../utils/domain.js';
|
|
10
11
|
import { CustomObjectsApi, KubeConfig } from '@kubernetes/client-node';
|
|
11
12
|
import { Injectable } from '@nestjs/common';
|
|
12
13
|
let KcpKubernetesService = class KcpKubernetesService {
|
|
13
14
|
k8sApi;
|
|
14
15
|
baseUrl;
|
|
15
16
|
constructor() {
|
|
16
|
-
const kubeConfigKcp = process.env
|
|
17
|
+
const kubeConfigKcp = process.env.KUBECONFIG_KCP;
|
|
17
18
|
const kc = new KubeConfig();
|
|
18
19
|
kc.loadFromFile(kubeConfigKcp);
|
|
19
20
|
kc.addUser({
|
|
@@ -46,10 +47,23 @@ let KcpKubernetesService = class KcpKubernetesService {
|
|
|
46
47
|
const path = this.buildWorkspacePath(organization, account);
|
|
47
48
|
return new URL(`${this.baseUrl.origin}/clusters/${path}`);
|
|
48
49
|
}
|
|
49
|
-
getKcpWorkspacePublicUrl(
|
|
50
|
+
getKcpWorkspacePublicUrl(request) {
|
|
51
|
+
const organization = getOrganization(request);
|
|
52
|
+
const account = request.query?.['core_platform-mesh_io_account'];
|
|
50
53
|
const path = this.buildWorkspacePath(organization, account);
|
|
51
|
-
const baseDomain = process.env
|
|
52
|
-
|
|
54
|
+
const baseDomain = process.env.BASE_DOMAINS_DEFAULT;
|
|
55
|
+
const port = this.getAppPort(request);
|
|
56
|
+
return `https://kcp.api.${baseDomain}${port}/clusters/${path}`;
|
|
57
|
+
}
|
|
58
|
+
getAppPort(request) {
|
|
59
|
+
const forwardedPort = request.headers['x-forwarded-port'];
|
|
60
|
+
const forwardedPortValue = Array.isArray(forwardedPort)
|
|
61
|
+
? forwardedPort[0]
|
|
62
|
+
: forwardedPort;
|
|
63
|
+
const requestHostPort = request.headers.host?.split(':')[1];
|
|
64
|
+
const portFromRequest = process.env.FRONTEND_PORT || forwardedPortValue || requestHostPort || '';
|
|
65
|
+
const isStandardOrEmptyPort = portFromRequest === '80' || portFromRequest === '443' || !portFromRequest;
|
|
66
|
+
return isStandardOrEmptyPort ? '' : `:${portFromRequest}`;
|
|
53
67
|
}
|
|
54
68
|
};
|
|
55
69
|
KcpKubernetesService = __decorate([
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kcp-k8s.service.js","sourceRoot":"","sources":["../../../src/portal-options/services/kcp-k8s.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"kcp-k8s.service.js","sourceRoot":"","sources":["../../../src/portal-options/services/kcp-k8s.service.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platform-mesh/portal-server-lib",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.14",
|
|
4
4
|
"author": "Platform Mesh",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -57,14 +57,14 @@
|
|
|
57
57
|
"rxjs": "7.8.2"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@eslint/js": "9.39.
|
|
60
|
+
"@eslint/js": "9.39.1",
|
|
61
61
|
"@nestjs/cli": "11.0.10",
|
|
62
62
|
"@nestjs/testing": "11.1.8",
|
|
63
63
|
"@openmfp/config-prettier": "0.9.1",
|
|
64
64
|
"@types/jest": "30.0.0",
|
|
65
65
|
"@types/node": "24.10.0",
|
|
66
66
|
"@types/supertest": "6.0.3",
|
|
67
|
-
"eslint": "9.39.
|
|
67
|
+
"eslint": "9.39.1",
|
|
68
68
|
"eslint-config-prettier": "10.1.8",
|
|
69
69
|
"globals": "16.5.0",
|
|
70
70
|
"jest": "30.2.0",
|
|
@@ -21,15 +21,9 @@ export class PMPortalContextService implements PortalContextProvider {
|
|
|
21
21
|
return portalContext;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
private addKcpWorkspaceUrl(
|
|
25
|
-
request: Request,
|
|
26
|
-
portalContext: PortalContext,
|
|
27
|
-
) {
|
|
28
|
-
const organization = getOrganization(request);
|
|
29
|
-
const account = request.query?.['core_platform-mesh_io_account'];
|
|
30
|
-
|
|
24
|
+
private addKcpWorkspaceUrl(request: Request, portalContext: PortalContext) {
|
|
31
25
|
portalContext.kcpWorkspaceUrl =
|
|
32
|
-
this.kcpKubernetesService.getKcpWorkspacePublicUrl(
|
|
26
|
+
this.kcpKubernetesService.getKcpWorkspacePublicUrl(request);
|
|
33
27
|
}
|
|
34
28
|
|
|
35
29
|
private processDynamicApiUrls(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { KcpKubernetesService } from './kcp-k8s.service.js';
|
|
2
|
+
import type { Request } from 'express';
|
|
2
3
|
|
|
3
4
|
jest.mock('@kubernetes/client-node', () => {
|
|
4
5
|
const makeApiClient = jest.fn(() => ({}));
|
|
@@ -19,6 +20,10 @@ jest.mock('@kubernetes/client-node', () => {
|
|
|
19
20
|
};
|
|
20
21
|
});
|
|
21
22
|
|
|
23
|
+
jest.mock('../utils/domain.js', () => ({
|
|
24
|
+
getOrganization: jest.fn(() => 'org-1'),
|
|
25
|
+
}));
|
|
26
|
+
|
|
22
27
|
describe('KcpKubernetesService', () => {
|
|
23
28
|
const OLD_ENV = process.env;
|
|
24
29
|
|
|
@@ -60,19 +65,143 @@ describe('KcpKubernetesService', () => {
|
|
|
60
65
|
);
|
|
61
66
|
});
|
|
62
67
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const svc = new KcpKubernetesService();
|
|
66
|
-
expect(svc.getKcpWorkspacePublicUrl('org1', 'acc1')).toBe(
|
|
67
|
-
'https://kcp.api.example.com/clusters/root:orgs:org1:acc1',
|
|
68
|
-
);
|
|
69
|
-
});
|
|
68
|
+
describe('KcpKubernetesService - getKcpWorkspacePublicUrl', () => {
|
|
69
|
+
const ORIGINAL_ENV = process.env;
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
);
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
jest.resetModules();
|
|
73
|
+
process.env = { ...ORIGINAL_ENV };
|
|
74
|
+
process.env.BASE_DOMAINS_DEFAULT = 'example.com';
|
|
75
|
+
process.env.KUBECONFIG_KCP = __filename; // loadFromFile is called; path must exist
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
process.env = ORIGINAL_ENV;
|
|
80
|
+
jest.restoreAllMocks();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const createService = () => {
|
|
84
|
+
// Mock KubeConfig internals to avoid reading real kubeconfig/server
|
|
85
|
+
const loadFromFile = jest.fn();
|
|
86
|
+
const addUser = jest.fn();
|
|
87
|
+
const addContext = jest.fn();
|
|
88
|
+
const setCurrentContext = jest.fn();
|
|
89
|
+
const getCurrentCluster = jest.fn(
|
|
90
|
+
() => ({ name: 'c', server: 'https://kcp.internal' }) as any,
|
|
91
|
+
);
|
|
92
|
+
const makeApiClient = jest.fn(() => ({}));
|
|
93
|
+
|
|
94
|
+
jest.doMock('@kubernetes/client-node', () => {
|
|
95
|
+
return {
|
|
96
|
+
KubeConfig: jest.fn().mockImplementation(() => ({
|
|
97
|
+
loadFromFile,
|
|
98
|
+
addUser,
|
|
99
|
+
addContext,
|
|
100
|
+
setCurrentContext,
|
|
101
|
+
getCurrentCluster,
|
|
102
|
+
makeApiClient,
|
|
103
|
+
})),
|
|
104
|
+
CustomObjectsApi: jest.fn(),
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Re-require module to use mocked client-node
|
|
109
|
+
const { KcpKubernetesService: Svc } = require('./kcp-k8s.service');
|
|
110
|
+
return new Svc() as KcpKubernetesService;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const makeReq = (overrides: Partial<Request> = {}): Request =>
|
|
114
|
+
({
|
|
115
|
+
headers: {},
|
|
116
|
+
query: {},
|
|
117
|
+
...overrides,
|
|
118
|
+
}) as unknown as Request;
|
|
119
|
+
|
|
120
|
+
it('builds URL with organization and account from query', () => {
|
|
121
|
+
const svc = createService();
|
|
122
|
+
const req = makeReq({
|
|
123
|
+
query: { 'core_platform-mesh_io_account': 'acc-1' } as any,
|
|
124
|
+
headers: { host: 'kcp.api.example.com' } as any,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const url = svc.getKcpWorkspacePublicUrl(req);
|
|
128
|
+
expect(url).toBe(
|
|
129
|
+
'https://kcp.api.example.com/clusters/root:orgs:org-1:acc-1',
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('omits port for standard ports 80/443 and empty', () => {
|
|
134
|
+
const svc = createService();
|
|
135
|
+
|
|
136
|
+
let req = makeReq({
|
|
137
|
+
query: { 'core_platform-mesh_io_account': 'acc' } as any,
|
|
138
|
+
headers: { host: 'kcp.api.example.com:80' } as any,
|
|
139
|
+
});
|
|
140
|
+
expect(svc.getKcpWorkspacePublicUrl(req)).toBe(
|
|
141
|
+
'https://kcp.api.example.com/clusters/root:orgs:org-1:acc',
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
req = makeReq({
|
|
145
|
+
query: { 'core_platform-mesh_io_account': 'acc' } as any,
|
|
146
|
+
headers: {
|
|
147
|
+
'x-forwarded-port': '443',
|
|
148
|
+
host: 'kcp.api.example.com',
|
|
149
|
+
} as any,
|
|
150
|
+
});
|
|
151
|
+
expect(svc.getKcpWorkspacePublicUrl(req)).toBe(
|
|
152
|
+
'https://kcp.api.example.com/clusters/root:orgs:org-1:acc',
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
req = makeReq({
|
|
156
|
+
query: { 'core_platform-mesh_io_account': 'acc' } as any,
|
|
157
|
+
headers: { host: 'kcp.api.example.com' } as any,
|
|
158
|
+
});
|
|
159
|
+
expect(svc.getKcpWorkspacePublicUrl(req)).toBe(
|
|
160
|
+
'https://kcp.api.example.com/clusters/root:orgs:org-1:acc',
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('appends non-standard port from x-forwarded-port', () => {
|
|
165
|
+
const svc = createService();
|
|
166
|
+
const req = makeReq({
|
|
167
|
+
query: { 'core_platform-mesh_io_account': 'acc' } as any,
|
|
168
|
+
headers: {
|
|
169
|
+
'x-forwarded-port': '8443',
|
|
170
|
+
host: 'kcp.api.example.com',
|
|
171
|
+
} as any,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const url = svc.getKcpWorkspacePublicUrl(req);
|
|
175
|
+
expect(url).toBe(
|
|
176
|
+
'https://kcp.api.example.com:8443/clusters/root:orgs:org-1:acc',
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('falls back to port from host header when x-forwarded-port not present', () => {
|
|
181
|
+
const svc = createService();
|
|
182
|
+
const req = makeReq({
|
|
183
|
+
query: { 'core_platform-mesh_io_account': 'acc' } as any,
|
|
184
|
+
headers: { host: 'kcp.api.example.com:3000' } as any,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const url = svc.getKcpWorkspacePublicUrl(req);
|
|
188
|
+
expect(url).toBe(
|
|
189
|
+
'https://kcp.api.example.com:3000/clusters/root:orgs:org-1:acc',
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('uses FRONTEND_PORT env when provided', () => {
|
|
194
|
+
process.env.FRONTEND_PORT = '4200';
|
|
195
|
+
const svc = createService();
|
|
196
|
+
const req = makeReq({
|
|
197
|
+
query: { 'core_platform-mesh_io_account': 'acc' } as any,
|
|
198
|
+
headers: { host: 'kcp.api.example.com' } as any,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const url = svc.getKcpWorkspacePublicUrl(req);
|
|
202
|
+
expect(url).toBe(
|
|
203
|
+
'https://kcp.api.example.com:4200/clusters/root:orgs:org-1:acc',
|
|
204
|
+
);
|
|
205
|
+
});
|
|
77
206
|
});
|
|
78
207
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { getOrganization } from '../utils/domain.js';
|
|
1
2
|
import { CustomObjectsApi, KubeConfig } from '@kubernetes/client-node';
|
|
2
3
|
import { Injectable } from '@nestjs/common';
|
|
4
|
+
import type { Request } from 'express';
|
|
3
5
|
|
|
4
6
|
@Injectable()
|
|
5
7
|
export class KcpKubernetesService {
|
|
@@ -7,7 +9,7 @@ export class KcpKubernetesService {
|
|
|
7
9
|
private readonly baseUrl: URL;
|
|
8
10
|
|
|
9
11
|
constructor() {
|
|
10
|
-
const kubeConfigKcp = process.env
|
|
12
|
+
const kubeConfigKcp = process.env.KUBECONFIG_KCP;
|
|
11
13
|
const kc = new KubeConfig();
|
|
12
14
|
kc.loadFromFile(kubeConfigKcp);
|
|
13
15
|
// Temporary change to test.
|
|
@@ -48,9 +50,28 @@ export class KcpKubernetesService {
|
|
|
48
50
|
return new URL(`${this.baseUrl.origin}/clusters/${path}`);
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
getKcpWorkspacePublicUrl(
|
|
53
|
+
getKcpWorkspacePublicUrl(request: Request) {
|
|
54
|
+
const organization = getOrganization(request);
|
|
55
|
+
const account = request.query?.['core_platform-mesh_io_account'];
|
|
52
56
|
const path = this.buildWorkspacePath(organization, account);
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
|
|
58
|
+
const baseDomain = process.env.BASE_DOMAINS_DEFAULT;
|
|
59
|
+
const port = this.getAppPort(request);
|
|
60
|
+
|
|
61
|
+
return `https://kcp.api.${baseDomain}${port}/clusters/${path}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private getAppPort(request: Request): string {
|
|
65
|
+
const forwardedPort = request.headers['x-forwarded-port'];
|
|
66
|
+
const forwardedPortValue = Array.isArray(forwardedPort)
|
|
67
|
+
? forwardedPort[0]
|
|
68
|
+
: forwardedPort;
|
|
69
|
+
const requestHostPort = request.headers.host?.split(':')[1];
|
|
70
|
+
const portFromRequest =
|
|
71
|
+
process.env.FRONTEND_PORT || forwardedPortValue || requestHostPort || '';
|
|
72
|
+
|
|
73
|
+
const isStandardOrEmptyPort =
|
|
74
|
+
portFromRequest === '80' || portFromRequest === '443' || !portFromRequest;
|
|
75
|
+
return isStandardOrEmptyPort ? '' : `:${portFromRequest}`;
|
|
55
76
|
}
|
|
56
77
|
}
|