@platform-mesh/portal-server-lib 0.0.0 → 0.5.3
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/.github/workflows/pipeline.yaml +20 -0
- package/.prettierrc.mjs +6 -0
- package/CODEOWNERS +4 -0
- package/CODE_OF_CONDUCT.md +86 -0
- package/CONTRIBUTING.md +40 -0
- package/LICENSE +201 -0
- package/LICENSES/Apache-2.0.txt +73 -0
- package/README.md +41 -0
- package/base.jest.config.js +15 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/portal-options/account-entity-context-provider.service.d.ts +4 -0
- package/dist/portal-options/account-entity-context-provider.service.js +35 -0
- package/dist/portal-options/account-entity-context-provider.service.js.map +1 -0
- package/dist/portal-options/auth-callback-provider.d.ts +10 -0
- package/dist/portal-options/auth-callback-provider.js +36 -0
- package/dist/portal-options/auth-callback-provider.js.map +1 -0
- package/dist/portal-options/auth-config-provider.d.ts +9 -0
- package/dist/portal-options/auth-config-provider.js +72 -0
- package/dist/portal-options/auth-config-provider.js.map +1 -0
- package/dist/portal-options/index.d.ts +10 -0
- package/dist/portal-options/index.js +11 -0
- package/dist/portal-options/index.js.map +1 -0
- package/dist/portal-options/logout-callback.service.d.ts +12 -0
- package/dist/portal-options/logout-callback.service.js +63 -0
- package/dist/portal-options/logout-callback.service.js.map +1 -0
- package/dist/portal-options/models/luigi-context.d.ts +4 -0
- package/dist/portal-options/models/luigi-context.js +2 -0
- package/dist/portal-options/models/luigi-context.js.map +1 -0
- package/dist/portal-options/pm-portal-context.service.d.ts +11 -0
- package/dist/portal-options/pm-portal-context.service.js +50 -0
- package/dist/portal-options/pm-portal-context.service.js.map +1 -0
- package/dist/portal-options/pm-request-context-provider.d.ts +13 -0
- package/dist/portal-options/pm-request-context-provider.js +34 -0
- package/dist/portal-options/pm-request-context-provider.js.map +1 -0
- package/dist/portal-options/service-providers/content-configuration-service-providers.service.d.ts +5 -0
- package/dist/portal-options/service-providers/content-configuration-service-providers.service.js +83 -0
- package/dist/portal-options/service-providers/content-configuration-service-providers.service.js.map +1 -0
- package/dist/portal-options/service-providers/contentconfigurations-query.d.ts +1 -0
- package/dist/portal-options/service-providers/contentconfigurations-query.js +22 -0
- package/dist/portal-options/service-providers/contentconfigurations-query.js.map +1 -0
- package/dist/portal-options/service-providers/kubernetes-service-providers.service.d.ts +8 -0
- package/dist/portal-options/service-providers/kubernetes-service-providers.service.js +98 -0
- package/dist/portal-options/service-providers/kubernetes-service-providers.service.js.map +1 -0
- package/dist/portal-options/service-providers/models/contentconfigurations.d.ts +20 -0
- package/dist/portal-options/service-providers/models/contentconfigurations.js +2 -0
- package/dist/portal-options/service-providers/models/contentconfigurations.js.map +1 -0
- package/dist/portal-options/service-providers/models/welcome-node-config.d.ts +2 -0
- package/dist/portal-options/service-providers/models/welcome-node-config.js +35 -0
- package/dist/portal-options/service-providers/models/welcome-node-config.js.map +1 -0
- package/dist/portal-options/services/iam-graphql.service.d.ts +7 -0
- package/dist/portal-options/services/iam-graphql.service.js +40 -0
- package/dist/portal-options/services/iam-graphql.service.js.map +1 -0
- package/dist/portal-options/services/kcp-k8s.service.d.ts +11 -0
- package/dist/portal-options/services/kcp-k8s.service.js +60 -0
- package/dist/portal-options/services/kcp-k8s.service.js.map +1 -0
- package/dist/portal-options/services/queries.d.ts +1 -0
- package/dist/portal-options/services/queries.js +7 -0
- package/dist/portal-options/services/queries.js.map +1 -0
- package/dist/portal-options/utils/domain.d.ts +3 -0
- package/dist/portal-options/utils/domain.js +11 -0
- package/dist/portal-options/utils/domain.js.map +1 -0
- package/eslint.config.mjs +27 -0
- package/jest.config.ts +42 -0
- package/nest-cli.json +6 -0
- package/package.json +85 -2
- package/renovate.json +6 -0
- package/src/index.ts +1 -0
- package/src/portal-options/account-entity-context-provider.service.ts +30 -0
- package/src/portal-options/auth-callback-provider.spec.ts +85 -0
- package/src/portal-options/auth-callback-provider.ts +27 -0
- package/src/portal-options/auth-config-provider.spec.ts +101 -0
- package/src/portal-options/auth-config-provider.ts +82 -0
- package/src/portal-options/index.ts +11 -0
- package/src/portal-options/logout-callback.service.spec.ts +113 -0
- package/src/portal-options/logout-callback.service.ts +60 -0
- package/src/portal-options/models/luigi-context.ts +4 -0
- package/src/portal-options/pm-portal-context.service.spec.ts +155 -0
- package/src/portal-options/pm-portal-context.service.ts +63 -0
- package/src/portal-options/pm-request-context-provider.spec.ts +69 -0
- package/src/portal-options/pm-request-context-provider.ts +33 -0
- package/src/portal-options/service-providers/content-configuration-service-providers.service.spec.ts +157 -0
- package/src/portal-options/service-providers/content-configuration-service-providers.service.ts +130 -0
- package/src/portal-options/service-providers/contentconfigurations-query.ts +22 -0
- package/src/portal-options/service-providers/kubernetes-service-providers.service.spec.ts +197 -0
- package/src/portal-options/service-providers/kubernetes-service-providers.service.ts +115 -0
- package/src/portal-options/service-providers/models/contentconfigurations.ts +13 -0
- package/src/portal-options/service-providers/models/welcome-node-config.ts +36 -0
- package/src/portal-options/services/iam-graphql.service.spec.ts +77 -0
- package/src/portal-options/services/iam-graphql.service.ts +33 -0
- package/src/portal-options/services/kcp-k8s.service.spec.ts +78 -0
- package/src/portal-options/services/kcp-k8s.service.ts +56 -0
- package/src/portal-options/services/queries.ts +7 -0
- package/src/portal-options/utils/domain.spec.ts +114 -0
- package/src/portal-options/utils/domain.ts +13 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.json +18 -0
- package/tsconfig.test.json +3 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { PMRequestContextProvider } from '../pm-request-context-provider.js';
|
|
2
|
+
import { IAMGraphQlService } from './iam-graphql.service.js';
|
|
3
|
+
import { MUTATION_LOGIN } from './queries.js';
|
|
4
|
+
import { GraphQLClient } from 'graphql-request';
|
|
5
|
+
import { mock } from 'jest-mock-extended';
|
|
6
|
+
|
|
7
|
+
jest.mock('@kubernetes/client-node', () => {
|
|
8
|
+
class KubeConfig {
|
|
9
|
+
loadFromDefault = jest.fn();
|
|
10
|
+
loadFromFile = jest.fn();
|
|
11
|
+
getCurrentCluster = jest.fn().mockReturnValue({
|
|
12
|
+
server: 'https://k8s.example.com/base',
|
|
13
|
+
name: 'test-cluster',
|
|
14
|
+
});
|
|
15
|
+
makeApiClient = jest.fn();
|
|
16
|
+
addUser = jest.fn();
|
|
17
|
+
addContext = jest.fn();
|
|
18
|
+
setCurrentContext = jest.fn();
|
|
19
|
+
}
|
|
20
|
+
class CustomObjectsApi {}
|
|
21
|
+
return { KubeConfig, CustomObjectsApi };
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('IAMGraphQlService', () => {
|
|
25
|
+
const mockIamServiceApiUrl = 'http://localhost:8080/query';
|
|
26
|
+
let service: IAMGraphQlService;
|
|
27
|
+
const gqlClient = {
|
|
28
|
+
request: jest.fn(),
|
|
29
|
+
} as unknown as GraphQLClient;
|
|
30
|
+
|
|
31
|
+
const requestContextProvider = mock<PMRequestContextProvider>({
|
|
32
|
+
getContextValues: jest
|
|
33
|
+
.fn()
|
|
34
|
+
.mockResolvedValue({ iamServiceApiUrl: mockIamServiceApiUrl }),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
let GraphQLClientMock: jest.SpyInstance;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
jest.resetAllMocks();
|
|
41
|
+
|
|
42
|
+
// Re-apply RequestContextProvider mock after resetAllMocks
|
|
43
|
+
(
|
|
44
|
+
requestContextProvider.getContextValues as unknown as jest.Mock
|
|
45
|
+
).mockResolvedValue({
|
|
46
|
+
iamServiceApiUrl: mockIamServiceApiUrl,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Mock GraphQLClient constructor to return our mocked client
|
|
50
|
+
GraphQLClientMock = jest
|
|
51
|
+
.spyOn<any, any>(require('graphql-request'), 'GraphQLClient')
|
|
52
|
+
.mockImplementation(() => gqlClient);
|
|
53
|
+
|
|
54
|
+
service = new IAMGraphQlService(requestContextProvider as any);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should call mutation addUser', async () => {
|
|
58
|
+
(gqlClient.request as jest.Mock).mockResolvedValue('');
|
|
59
|
+
|
|
60
|
+
const response = await service.addUser('token', {} as any, {} as any);
|
|
61
|
+
|
|
62
|
+
expect(GraphQLClientMock).toHaveBeenCalledWith(mockIamServiceApiUrl, {
|
|
63
|
+
headers: { Authorization: 'Bearer token' },
|
|
64
|
+
});
|
|
65
|
+
expect(gqlClient.request).toHaveBeenCalledWith(MUTATION_LOGIN);
|
|
66
|
+
expect(response).toBe(undefined);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should call mutation addUser and log error', async () => {
|
|
70
|
+
console.error = jest.fn();
|
|
71
|
+
(gqlClient.request as jest.Mock).mockRejectedValue('error');
|
|
72
|
+
|
|
73
|
+
const response = await service.addUser('token', {} as any, {} as any);
|
|
74
|
+
expect(response).toBe(undefined);
|
|
75
|
+
expect(console.error).toHaveBeenCalledWith('error');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { PMRequestContextProvider } from '../pm-request-context-provider.js';
|
|
2
|
+
import { MUTATION_LOGIN } from './queries.js';
|
|
3
|
+
import { Injectable } from '@nestjs/common';
|
|
4
|
+
import type { Request, Response } from 'express';
|
|
5
|
+
import { GraphQLClient } from 'graphql-request';
|
|
6
|
+
|
|
7
|
+
@Injectable()
|
|
8
|
+
export class IAMGraphQlService {
|
|
9
|
+
constructor(private requestContextProvider: PMRequestContextProvider) {}
|
|
10
|
+
|
|
11
|
+
async addUser(
|
|
12
|
+
token: string,
|
|
13
|
+
request: Request,
|
|
14
|
+
response: Response,
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
const requestContext = await this.requestContextProvider.getContextValues(
|
|
17
|
+
request,
|
|
18
|
+
response,
|
|
19
|
+
);
|
|
20
|
+
const iamUrl = requestContext.iamServiceApiUrl;
|
|
21
|
+
const client = new GraphQLClient(iamUrl, {
|
|
22
|
+
headers: {
|
|
23
|
+
Authorization: `Bearer ${token}`,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await client.request(MUTATION_LOGIN);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.error(e);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { KcpKubernetesService } from './kcp-k8s.service.js';
|
|
2
|
+
|
|
3
|
+
jest.mock('@kubernetes/client-node', () => {
|
|
4
|
+
const makeApiClient = jest.fn(() => ({}));
|
|
5
|
+
const getCurrentCluster = jest.fn().mockReturnValue({
|
|
6
|
+
server: 'https://kcp.example.com/base',
|
|
7
|
+
name: 'test-cluster',
|
|
8
|
+
});
|
|
9
|
+
return {
|
|
10
|
+
CustomObjectsApi: jest.fn(),
|
|
11
|
+
KubeConfig: jest.fn().mockImplementation(() => ({
|
|
12
|
+
loadFromFile: jest.fn(),
|
|
13
|
+
addUser: jest.fn(),
|
|
14
|
+
addContext: jest.fn(),
|
|
15
|
+
setCurrentContext: jest.fn(),
|
|
16
|
+
getCurrentCluster,
|
|
17
|
+
makeApiClient,
|
|
18
|
+
})),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('KcpKubernetesService', () => {
|
|
23
|
+
const OLD_ENV = process.env;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.resetModules();
|
|
27
|
+
process.env = { ...OLD_ENV, KUBECONFIG_KCP: '/tmp/kcp.kubeconfig' };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterAll(() => {
|
|
31
|
+
process.env = OLD_ENV;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('initializes k8s client and baseUrl from kubeconfig', () => {
|
|
35
|
+
const svc = new KcpKubernetesService();
|
|
36
|
+
expect(svc.getKcpK8sApiClient()).toBeDefined();
|
|
37
|
+
expect(svc.getKcpWorkspaceUrl('org1', 'acc1').toString()).toBe(
|
|
38
|
+
'https://kcp.example.com/clusters/root:orgs:org1:acc1',
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('builds workspace url without account', () => {
|
|
43
|
+
const svc = new KcpKubernetesService();
|
|
44
|
+
expect(svc.getKcpWorkspaceUrl('org1', '').toString()).toBe(
|
|
45
|
+
'https://kcp.example.com/clusters/root:orgs:org1',
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('builds virtual workspace url with account', () => {
|
|
50
|
+
const svc = new KcpKubernetesService();
|
|
51
|
+
expect(svc.getKcpVirtualWorkspaceUrl('orgX', 'accY').toString()).toBe(
|
|
52
|
+
'https://kcp.example.com/services/contentconfigurations/clusters/root:orgs:orgX:accY',
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('builds virtual workspace url without account', () => {
|
|
57
|
+
const svc = new KcpKubernetesService();
|
|
58
|
+
expect(svc.getKcpVirtualWorkspaceUrl('orgX', '').toString()).toBe(
|
|
59
|
+
'https://kcp.example.com/services/contentconfigurations/clusters/root:orgs:orgX',
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('builds public workspace url with org and account', () => {
|
|
64
|
+
process.env['BASE_DOMAINS_DEFAULT'] = 'example.com';
|
|
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
|
+
});
|
|
70
|
+
|
|
71
|
+
it('builds public workspace url without account', () => {
|
|
72
|
+
process.env['BASE_DOMAINS_DEFAULT'] = 'example.com';
|
|
73
|
+
const svc = new KcpKubernetesService();
|
|
74
|
+
expect(svc.getKcpWorkspacePublicUrl('org1', '')).toBe(
|
|
75
|
+
'https://kcp.api.example.com/clusters/root:orgs:org1',
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { CustomObjectsApi, KubeConfig } from '@kubernetes/client-node';
|
|
2
|
+
import { Injectable } from '@nestjs/common';
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class KcpKubernetesService {
|
|
6
|
+
private readonly k8sApi: CustomObjectsApi;
|
|
7
|
+
private readonly baseUrl: URL;
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
const kubeConfigKcp = process.env['KUBECONFIG_KCP'];
|
|
11
|
+
const kc = new KubeConfig();
|
|
12
|
+
kc.loadFromFile(kubeConfigKcp);
|
|
13
|
+
// Temporary change to test.
|
|
14
|
+
kc.addUser({
|
|
15
|
+
name: 'oidc',
|
|
16
|
+
});
|
|
17
|
+
kc.addContext({
|
|
18
|
+
name: 'oidc',
|
|
19
|
+
user: 'oidc',
|
|
20
|
+
cluster: kc.getCurrentCluster()?.name || '',
|
|
21
|
+
});
|
|
22
|
+
kc.setCurrentContext('oidc');
|
|
23
|
+
this.baseUrl = new URL(kc.getCurrentCluster()?.server || '');
|
|
24
|
+
this.k8sApi = kc.makeApiClient(CustomObjectsApi);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getKcpK8sApiClient() {
|
|
28
|
+
return this.k8sApi;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private buildWorkspacePath(organization: string, account?: string) {
|
|
32
|
+
let path = `root:orgs:${organization}`;
|
|
33
|
+
if (account) {
|
|
34
|
+
path += `:${account}`; // FIXME: how are nested accounts and paths handled in the portal?
|
|
35
|
+
}
|
|
36
|
+
return path;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getKcpVirtualWorkspaceUrl(organization: string, account: string) {
|
|
40
|
+
const path = this.buildWorkspacePath(organization, account);
|
|
41
|
+
return new URL(
|
|
42
|
+
`${this.baseUrl.origin}/services/contentconfigurations/clusters/${path}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getKcpWorkspaceUrl(organization: string, account: string) {
|
|
47
|
+
const path = this.buildWorkspacePath(organization, account);
|
|
48
|
+
return new URL(`${this.baseUrl.origin}/clusters/${path}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getKcpWorkspacePublicUrl(organization: string, account: string) {
|
|
52
|
+
const path = this.buildWorkspacePath(organization, account);
|
|
53
|
+
const baseDomain = process.env['BASE_DOMAINS_DEFAULT'];
|
|
54
|
+
return `https://kcp.api.${baseDomain}/clusters/${path}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { getDiscoveryEndpoint, getOrganization } from './domain.js';
|
|
2
|
+
import type { Request } from 'express';
|
|
3
|
+
|
|
4
|
+
describe('getOrganization', () => {
|
|
5
|
+
const OLD_ENV = process.env;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
jest.resetModules();
|
|
9
|
+
process.env = { ...OLD_ENV };
|
|
10
|
+
process.env.OIDC_CLIENT_ID_DEFAULT = 'default-client';
|
|
11
|
+
process.env.BASE_DOMAINS_DEFAULT = 'example.com';
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterAll(() => {
|
|
15
|
+
process.env = OLD_ENV;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const makeReq = (hostname: string): Request =>
|
|
19
|
+
({ hostname }) as unknown as Request;
|
|
20
|
+
|
|
21
|
+
it('returns subdomain when hostname is not base domain', () => {
|
|
22
|
+
const req = makeReq('team1.example.com');
|
|
23
|
+
expect(getOrganization(req)).toBe('team1');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns client id when hostname equals base domain', () => {
|
|
27
|
+
const req = makeReq('example.com');
|
|
28
|
+
expect(getOrganization(req)).toBe('default-client');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('handles single-label hostname', () => {
|
|
32
|
+
const req = makeReq('localhost');
|
|
33
|
+
expect(getOrganization(req)).toBe('localhost');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('handles multi-level subdomain', () => {
|
|
37
|
+
const req = makeReq('alpha.beta.example.com');
|
|
38
|
+
expect(getOrganization(req)).toBe('alpha');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('reflects updated env values', () => {
|
|
42
|
+
process.env.OIDC_CLIENT_ID_DEFAULT = 'another-client';
|
|
43
|
+
process.env.BASE_DOMAINS_DEFAULT = 'corp.example.org';
|
|
44
|
+
expect(getOrganization(makeReq('corp.example.org'))).toBe('another-client');
|
|
45
|
+
expect(getOrganization(makeReq('dev.corp.example.org'))).toBe('dev');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('getDiscoveryEndpoint', () => {
|
|
50
|
+
const OLD_ENV = process.env;
|
|
51
|
+
|
|
52
|
+
const makeReq = (hostname: string): Request =>
|
|
53
|
+
({ hostname }) as unknown as Request;
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
jest.resetModules();
|
|
57
|
+
process.env = { ...OLD_ENV };
|
|
58
|
+
process.env.OIDC_CLIENT_ID_DEFAULT = 'default-client';
|
|
59
|
+
process.env.BASE_DOMAINS_DEFAULT = 'example.com';
|
|
60
|
+
process.env.DISCOVERY_ENDPOINT =
|
|
61
|
+
'https://idp.example.com/${org-name}/.well-known/openid-configuration';
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterAll(() => {
|
|
65
|
+
process.env = OLD_ENV;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('replaces ${org-name} with subdomain org from hostname', () => {
|
|
69
|
+
const req = makeReq('team1.example.com');
|
|
70
|
+
expect(getDiscoveryEndpoint(req)).toBe(
|
|
71
|
+
'https://idp.example.com/team1/.well-known/openid-configuration',
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('replaces ${org-name} with default client id when hostname equals base domain', () => {
|
|
76
|
+
const req = makeReq('example.com');
|
|
77
|
+
expect(getDiscoveryEndpoint(req)).toBe(
|
|
78
|
+
'https://idp.example.com/default-client/.well-known/openid-configuration',
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('uses first label for multi-level subdomains', () => {
|
|
83
|
+
const req = makeReq('alpha.beta.example.com');
|
|
84
|
+
expect(getDiscoveryEndpoint(req)).toBe(
|
|
85
|
+
'https://idp.example.com/alpha/.well-known/openid-configuration',
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns undefined when DISCOVERY_ENDPOINT is not set', () => {
|
|
90
|
+
delete process.env.DISCOVERY_ENDPOINT;
|
|
91
|
+
const req = makeReq('team1.example.com');
|
|
92
|
+
expect(getDiscoveryEndpoint(req)).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('returns template unchanged if it does not contain the placeholder', () => {
|
|
96
|
+
process.env.DISCOVERY_ENDPOINT = 'https://idp.example.com/fixed-path';
|
|
97
|
+
const req = makeReq('team1.example.com');
|
|
98
|
+
expect(getDiscoveryEndpoint(req)).toBe(
|
|
99
|
+
'https://idp.example.com/fixed-path',
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('reflects updated env and organization resolution', () => {
|
|
104
|
+
process.env.BASE_DOMAINS_DEFAULT = 'corp.example.org';
|
|
105
|
+
process.env.OIDC_CLIENT_ID_DEFAULT = 'corp-default';
|
|
106
|
+
process.env.DISCOVERY_ENDPOINT = 'https://auth.corp/${org-name}/discovery';
|
|
107
|
+
expect(getDiscoveryEndpoint(makeReq('corp.example.org'))).toBe(
|
|
108
|
+
'https://auth.corp/corp-default/discovery',
|
|
109
|
+
);
|
|
110
|
+
expect(getDiscoveryEndpoint(makeReq('dev.corp.example.org'))).toBe(
|
|
111
|
+
'https://auth.corp/dev/discovery',
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Request } from 'express';
|
|
2
|
+
|
|
3
|
+
export const getOrganization = (request: Request): string => {
|
|
4
|
+
const subDomain = request.hostname.split('.')[0];
|
|
5
|
+
const clientId = process.env['OIDC_CLIENT_ID_DEFAULT'];
|
|
6
|
+
const baseDomain = process.env['BASE_DOMAINS_DEFAULT'];
|
|
7
|
+
return request.hostname !== baseDomain ? subDomain : clientId;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const getDiscoveryEndpoint = (request: Request): string => {
|
|
11
|
+
const clientId = getOrganization(request);
|
|
12
|
+
return process.env[`DISCOVERY_ENDPOINT`]?.replace('${org-name}', clientId);
|
|
13
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "NodeNext",
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"allowSyntheticDefaultImports": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"baseUrl": "./",
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"removeComments": true,
|
|
13
|
+
"emitDecoratorMetadata": true,
|
|
14
|
+
"experimentalDecorators": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"skipLibCheck": true
|
|
17
|
+
},
|
|
18
|
+
}
|