diff --git a/package-lock.json b/package-lock.json index a84a4aa..7fde307 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@nestjs/axios": ">=3.0.0", "@nestjs/common": ">=10.0.0", "@nestjs/serve-static": ">=4.0.0", - "@openmfp/portal-server-lib": ">=0.160.1", + "@openmfp/portal-server-lib": ">=0.160.2", "axios": ">=1.7.7", "express": ">=4.21.1", "rxjs": ">=7.8.1" @@ -51,6 +51,7 @@ "@nestjs/core": ">=10.0.0", "@nestjs/platform-express": ">=10.0.0", "@nestjs/serve-static": ">=4.0.0", + "@openmfp/portal-server-lib": ">=0.160.2", "axios": ">=1.7.7", "cookie-parser": ">=1.4.7", "express": ">=4.21.1", diff --git a/package.json b/package.json index b221279..ec1e24a 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "@nestjs/core": ">=10.0.0", "@nestjs/platform-express": ">=10.0.0", "@nestjs/serve-static": ">=4.0.0", - "@openmfp/portal-server-lib": ">=0.160.1", + "@openmfp/portal-server-lib": ">=0.160.2", "axios": ">=1.7.7", "cookie-parser": ">=1.4.7", "express": ">=4.21.1", @@ -48,7 +48,7 @@ "@nestjs/axios": ">=3.0.0", "@nestjs/common": ">=10.0.0", "@nestjs/serve-static": ">=4.0.0", - "@openmfp/portal-server-lib": ">=0.160.1", + "@openmfp/portal-server-lib": ">=0.160.2", "axios": ">=1.7.7", "express": ">=4.21.1", "rxjs": ">=7.8.1" diff --git a/src/portal-options/auth-config-provider.spec.ts b/src/portal-options/auth-config-provider.spec.ts index 9296de7..07cbfc5 100644 --- a/src/portal-options/auth-config-provider.spec.ts +++ b/src/portal-options/auth-config-provider.spec.ts @@ -1,5 +1,4 @@ import { PMAuthConfigProvider } from './auth-config-provider.js'; -import { getDomainAndOrganization } from './utils/domain.js'; import { HttpException } from '@nestjs/common'; import { DiscoveryService, @@ -98,22 +97,4 @@ describe('PMAuthConfigProvider', () => { await expect(provider.getAuthConfig(req)).rejects.toThrow(HttpException); }); - - it('getDomain should return organization and baseDomain', () => { - const req = { hostname: 'foo.example.com' } as Request; - const result = getDomainAndOrganization(req); - expect(result).toEqual({ - organization: 'foo', - baseDomain: 'example.com', - }); - }); - - it('getDomain should return clientId if hostname equals baseDomain', () => { - const req = { hostname: 'example.com' } as Request; - const result = getDomainAndOrganization(req); - expect(result).toEqual({ - organization: 'client123', - baseDomain: 'example.com', - }); - }); }); diff --git a/src/portal-options/auth-config-provider.ts b/src/portal-options/auth-config-provider.ts index 10d258b..b524647 100644 --- a/src/portal-options/auth-config-provider.ts +++ b/src/portal-options/auth-config-provider.ts @@ -1,3 +1,4 @@ +import { getOrganization } from './utils/domain.js'; import { CoreV1Api, KubeConfig } from '@kubernetes/client-node'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { @@ -19,13 +20,7 @@ export class PMAuthConfigProvider implements AuthConfigService { async getAuthConfig(request: Request): Promise { const baseDomain = process.env['BASE_DOMAINS_DEFAULT']; - - const subDomain = request.hostname.split('.')[0]; - const isSubdomain = request.hostname !== baseDomain; - - const clientId = isSubdomain - ? subDomain - : process.env['OIDC_CLIENT_ID_DEFAULT']; + const clientId = getOrganization(request); const clientSecret = await this.getClientSecret(clientId); const oidcUrl = process.env[`DISCOVERY_ENDPOINT`]?.replace( diff --git a/src/portal-options/pm-portal-context.service.spec.ts b/src/portal-options/pm-portal-context.service.spec.ts index 56cb064..970e955 100644 --- a/src/portal-options/pm-portal-context.service.spec.ts +++ b/src/portal-options/pm-portal-context.service.spec.ts @@ -1,9 +1,10 @@ import { PMPortalContextService } from './pm-portal-context.service.js'; import { KcpKubernetesService } from './services/kcp-k8s.service.js'; -import { getDomainAndOrganization } from './utils/domain.js'; +import { getOrganization } from './utils/domain.js'; import { Test, TestingModule } from '@nestjs/testing'; import { Request } from 'express'; import { mock } from 'jest-mock-extended'; +import process from 'node:process'; jest.mock('@kubernetes/client-node', () => { class KubeConfig { @@ -23,22 +24,19 @@ jest.mock('@kubernetes/client-node', () => { }); jest.mock('./utils/domain.js', () => ({ - getDomainAndOrganization: jest.fn(), + getOrganization: jest.fn(), })); describe('PMPortalContextService', () => { let service: PMPortalContextService; let kcpKubernetesServiceMock: jest.Mocked; - const mockedGetDomainAndOrganization = jest.mocked(getDomainAndOrganization); + const mockedGetDomainAndOrganization = jest.mocked(getOrganization); let mockRequest: any; beforeEach(async () => { kcpKubernetesServiceMock = mock(); - mockedGetDomainAndOrganization.mockReturnValue({ - baseDomain: 'example.com', - organization: 'test-org', - }); + mockedGetDomainAndOrganization.mockReturnValue('test-org'); const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -87,10 +85,7 @@ describe('PMPortalContextService', () => { process.env.OTHER_ENV_VAR = 'should-be-ignored'; try { - mockedGetDomainAndOrganization.mockReturnValue({ - baseDomain: 'example.com', - organization: 'test-org', - }); + mockedGetDomainAndOrganization.mockReturnValue('test-org'); const result = await service.getContextValues(mockRequest as Request); @@ -110,10 +105,7 @@ describe('PMPortalContextService', () => { process.env.OPENMFP_PORTAL_CONTEXT_MULTIPLE_SNAKE_CASE_KEYS = 'value2'; try { - mockedGetDomainAndOrganization.mockReturnValue({ - baseDomain: 'example.com', - organization: 'test-org', - }); + mockedGetDomainAndOrganization.mockReturnValue('test-org'); const result = await service.getContextValues(mockRequest as Request); @@ -132,10 +124,7 @@ describe('PMPortalContextService', () => { 'https://${org-subdomain}api.example.com/${org-name}/graphql'; try { - mockedGetDomainAndOrganization.mockReturnValue({ - baseDomain: 'example.com', - organization: 'test-org', - }); + mockedGetDomainAndOrganization.mockReturnValue('test-org'); mockRequest.hostname = 'subdomain.example.com'; @@ -154,11 +143,8 @@ describe('PMPortalContextService', () => { 'https://${org-subdomain}api.example.com/${org-name}/graphql'; try { - mockedGetDomainAndOrganization.mockReturnValue({ - baseDomain: 'example.com', - organization: 'test-org', - }); - + mockedGetDomainAndOrganization.mockReturnValue('test-org'); + process.env['BASE_DOMAINS_DEFAULT'] = 'example.com'; mockRequest.hostname = 'example.com'; const result = await service.getContextValues(mockRequest as Request); @@ -177,10 +163,7 @@ describe('PMPortalContextService', () => { process.env.OPENMFP_PORTAL_CONTEXT_VALID_KEY = 'valid-value'; try { - mockedGetDomainAndOrganization.mockReturnValue({ - baseDomain: 'example.com', - organization: 'test-org', - }); + mockedGetDomainAndOrganization.mockReturnValue('test-org'); const result = await service.getContextValues(mockRequest as Request); diff --git a/src/portal-options/pm-portal-context.service.ts b/src/portal-options/pm-portal-context.service.ts index 632ec09..171cc26 100644 --- a/src/portal-options/pm-portal-context.service.ts +++ b/src/portal-options/pm-portal-context.service.ts @@ -1,5 +1,5 @@ import { KcpKubernetesService } from './services/kcp-k8s.service.js'; -import { getDomainAndOrganization } from './utils/domain.js'; +import { getOrganization } from './utils/domain.js'; import { Injectable } from '@nestjs/common'; import { PortalContextProvider } from '@openmfp/portal-server-lib'; import type { Request } from 'express'; @@ -31,7 +31,7 @@ export class PMPortalContextService implements PortalContextProvider { } private addKcpWorkspaceUrl(request, portalContext) { - const { organization } = getDomainAndOrganization(request); + const organization = getOrganization(request); const account = request.query?.['core_platform-mesh_io_account']; portalContext.kcpWorkspaceUrl = this.kcpKubernetesService @@ -43,12 +43,12 @@ export class PMPortalContextService implements PortalContextProvider { request: Request, portalContext: Record, ): void { - const org = getDomainAndOrganization(request); - const subDomain = - request.hostname === org.baseDomain ? '' : `${org.organization}.`; + const org = getOrganization(request); + const baseDomain = process.env['BASE_DOMAINS_DEFAULT']; + const subDomain = request.hostname !== baseDomain ? `${org}.` : ''; portalContext.crdGatewayApiUrl = portalContext.crdGatewayApiUrl ?.replace('${org-subdomain}', subDomain) - .replace('${org-name}', org.organization); + .replace('${org-name}', org); } private toCamelCase(text: string): string { diff --git a/src/portal-options/pm-request-context-provider.spec.ts b/src/portal-options/pm-request-context-provider.spec.ts index 507ef57..793af5e 100644 --- a/src/portal-options/pm-request-context-provider.spec.ts +++ b/src/portal-options/pm-request-context-provider.spec.ts @@ -1,6 +1,6 @@ import { PMPortalContextService } from './pm-portal-context.service.js'; import { PMRequestContextProvider } from './pm-request-context-provider.js'; -import { getDomainAndOrganization } from './utils/domain.js'; +import { getOrganization } from './utils/domain.js'; import type { Request } from 'express'; import { mock } from 'jest-mock-extended'; @@ -22,20 +22,17 @@ jest.mock('@kubernetes/client-node', () => { }); jest.mock('./utils/domain.js', () => ({ - getDomainAndOrganization: jest.fn(), + getOrganization: jest.fn(), })); describe('PMRequestContextProvider', () => { let provider: PMRequestContextProvider; const portalContextService = mock(); - const mockedGetDomainAndOrganization = jest.mocked(getDomainAndOrganization); + const mockedGetOrganization = jest.mocked(getOrganization); beforeEach(() => { jest.resetAllMocks(); - mockedGetDomainAndOrganization.mockReturnValue({ - organization: 'org1', - baseDomain: 'org1.example.com', - }); + mockedGetOrganization.mockReturnValue('org1'); ( portalContextService.getContextValues as unknown as jest.Mock ).mockResolvedValue({ @@ -62,7 +59,7 @@ describe('PMRequestContextProvider', () => { organization: 'org1', }); - expect(mockedGetDomainAndOrganization).toHaveBeenCalledWith(req); + expect(mockedGetOrganization).toHaveBeenCalledWith(req); expect(portalContextService.getContextValues).toHaveBeenCalledWith(req); }); }); diff --git a/src/portal-options/pm-request-context-provider.ts b/src/portal-options/pm-request-context-provider.ts index 42e6f6b..3f1bfb3 100644 --- a/src/portal-options/pm-request-context-provider.ts +++ b/src/portal-options/pm-request-context-provider.ts @@ -1,5 +1,5 @@ import { PMPortalContextService } from './pm-portal-context.service.js'; -import { getDomainAndOrganization } from './utils/domain.js'; +import { getOrganization } from './utils/domain.js'; import { Injectable } from '@nestjs/common'; import { RequestContextProvider } from '@openmfp/portal-server-lib'; import type { Request } from 'express'; @@ -16,12 +16,13 @@ export class PMRequestContextProvider implements RequestContextProvider { constructor(private pmPortalContextService: PMPortalContextService) {} async getContextValues(request: Request): Promise { - const domainData = getDomainAndOrganization(request); + const organization = getOrganization(request); + const baseDomain = process.env['BASE_DOMAINS_DEFAULT']; return { ...request.query, ...(await this.pmPortalContextService.getContextValues(request)), - organization: domainData.organization, - isSubDomain: request.hostname !== domainData.baseDomain, + organization, + isSubDomain: request.hostname !== baseDomain, }; } } diff --git a/src/portal-options/service-providers/content-configuration-service-providers.service.spec.ts b/src/portal-options/service-providers/content-configuration-service-providers.service.spec.ts index 2c2087a..a0e2dbe 100644 --- a/src/portal-options/service-providers/content-configuration-service-providers.service.spec.ts +++ b/src/portal-options/service-providers/content-configuration-service-providers.service.spec.ts @@ -56,6 +56,17 @@ describe('ContentConfigurationServiceProvidersService', () => { expect(result).toEqual(welcomeNodeConfig); }); + it('throws if context organization is missing', async () => { + context.isSubDomain = false; + const result = await service.getServiceProviders( + 'token', + ['entity'], + context, + ); + + expect(result).toEqual(welcomeNodeConfig); + }); + it('returns parsed content configurations', async () => { mockClient.request.mockResolvedValue({ ui_platform_mesh_io: { diff --git a/src/portal-options/service-providers/content-configuration-service-providers.service.ts b/src/portal-options/service-providers/content-configuration-service-providers.service.ts index 38ee14d..540be03 100644 --- a/src/portal-options/service-providers/content-configuration-service-providers.service.ts +++ b/src/portal-options/service-providers/content-configuration-service-providers.service.ts @@ -2,6 +2,7 @@ import { RequestContext } from '../pm-request-context-provider.js'; import { contentConfigurationsQuery } from './contentconfigurations-query.js'; import { ContentConfigurationQueryResponse } from './models/contentconfigurations.js'; import { welcomeNodeConfig } from './models/welcome-node-config.js'; +import { Injectable } from '@nestjs/common'; import { ContentConfiguration, ServiceProviderResponse, @@ -9,6 +10,7 @@ import { } from '@openmfp/portal-server-lib'; import { GraphQLClient } from 'graphql-request'; +@Injectable() export class ContentConfigurationServiceProvidersService implements ServiceProviderService { diff --git a/src/portal-options/service-providers/models/welcome-node-config.ts b/src/portal-options/service-providers/models/welcome-node-config.ts index 3b1a5c0..bfce8e8 100644 --- a/src/portal-options/service-providers/models/welcome-node-config.ts +++ b/src/portal-options/service-providers/models/welcome-node-config.ts @@ -18,6 +18,7 @@ export const welcomeNodeConfig: ServiceProviderResponse = { pathSegment: 'welcome', hideFromNav: true, hideSideNav: true, + showBreadcrumbs: false, order: 1, url: '/assets/platform-mesh-portal-ui-wc.js#welcome-view', webcomponent: { diff --git a/src/portal-options/utils/domain.spec.ts b/src/portal-options/utils/domain.spec.ts index b1c5d39..8695bc3 100644 --- a/src/portal-options/utils/domain.spec.ts +++ b/src/portal-options/utils/domain.spec.ts @@ -1,7 +1,7 @@ -import { getDomainAndOrganization } from './domain.js'; +import { getOrganization } from './domain.js'; import type { Request } from 'express'; -describe('getDomainAndOrganization', () => { +describe('getOrganization', () => { const OLD_ENV = process.env; beforeEach(() => { @@ -18,56 +18,30 @@ describe('getDomainAndOrganization', () => { const makeReq = (hostname: string): Request => ({ hostname }) as unknown as Request; - it('returns subdomain as organization when hostname is not base domain', () => { - const req = makeReq('test-org.example.com'); - const result = getDomainAndOrganization(req); - expect(result).toEqual({ - organization: 'test-org', - baseDomain: 'example.com', - }); + it('returns subdomain when hostname is not base domain', () => { + const req = makeReq('team1.example.com'); + expect(getOrganization(req)).toBe('team1'); }); - it('returns client id as organization when hostname equals base domain', () => { + it('returns client id when hostname equals base domain', () => { const req = makeReq('example.com'); - const result = getDomainAndOrganization(req); - expect(result).toEqual({ - organization: 'default-client', - baseDomain: 'example.com', - }); + expect(getOrganization(req)).toBe('default-client'); }); - it('handles single-label hostnames', () => { + it('handles single-label hostname', () => { const req = makeReq('localhost'); - const result = getDomainAndOrganization(req); - expect(result).toEqual({ - organization: 'localhost', - baseDomain: 'example.com', - }); + expect(getOrganization(req)).toBe('localhost'); }); - it('handles multi-level subdomains', () => { + it('handles multi-level subdomain', () => { const req = makeReq('alpha.beta.example.com'); - const result = getDomainAndOrganization(req); - expect(result).toEqual({ - organization: 'alpha', - baseDomain: 'example.com', - }); + expect(getOrganization(req)).toBe('alpha'); }); - it('propagates updated env values', () => { + it('reflects updated env values', () => { process.env.OIDC_CLIENT_ID_DEFAULT = 'another-client'; process.env.BASE_DOMAINS_DEFAULT = 'corp.example.org'; - const req1 = makeReq('corp.example.org'); - const req2 = makeReq('dev.corp.example.org'); - - expect(getDomainAndOrganization(req1)).toEqual({ - organization: 'another-client', - baseDomain: 'corp.example.org', - }); - - expect(getDomainAndOrganization(req2)).toEqual({ - organization: 'dev', - baseDomain: 'corp.example.org', - }); + expect(getOrganization(makeReq('corp.example.org'))).toBe('another-client'); + expect(getOrganization(makeReq('dev.corp.example.org'))).toBe('dev'); }); }); diff --git a/src/portal-options/utils/domain.ts b/src/portal-options/utils/domain.ts index df8256f..7fc36c0 100644 --- a/src/portal-options/utils/domain.ts +++ b/src/portal-options/utils/domain.ts @@ -1,13 +1,8 @@ import type { Request } from 'express'; -export const getDomainAndOrganization = ( - request: Request, -): { organization?: string; baseDomain?: string } => { +export const getOrganization = (request: Request): string => { const subDomain = request.hostname.split('.')[0]; const clientId = process.env['OIDC_CLIENT_ID_DEFAULT']; const baseDomain = process.env['BASE_DOMAINS_DEFAULT']; - return { - organization: request.hostname === baseDomain ? clientId : subDomain, - baseDomain, - }; + return request.hostname !== baseDomain ? subDomain : clientId; };