Skip to content
19 changes: 0 additions & 19 deletions src/portal-options/auth-config-provider.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { PMAuthConfigProvider } from './auth-config-provider.js';
import { getDomainAndOrganization } from './utils/domain.js';
import { HttpException } from '@nestjs/common';
import {
DiscoveryService,
Expand Down Expand Up @@ -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',
});
});
});
9 changes: 2 additions & 7 deletions src/portal-options/auth-config-provider.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -19,13 +20,7 @@ export class PMAuthConfigProvider implements AuthConfigService {

async getAuthConfig(request: Request): Promise<ServerAuthVariables> {
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(
Expand Down
39 changes: 11 additions & 28 deletions src/portal-options/pm-portal-context.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<KcpKubernetesService>;
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: [
Expand Down Expand Up @@ -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);

Expand All @@ -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);

Expand All @@ -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';

Expand All @@ -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);
Expand All @@ -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);

Expand Down
12 changes: 6 additions & 6 deletions src/portal-options/pm-portal-context.service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -43,12 +43,12 @@ export class PMPortalContextService implements PortalContextProvider {
request: Request,
portalContext: Record<string, any>,
): 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 {
Expand Down
13 changes: 5 additions & 8 deletions src/portal-options/pm-request-context-provider.spec.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<PMPortalContextService>();
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({
Expand All @@ -62,7 +59,7 @@ describe('PMRequestContextProvider', () => {
organization: 'org1',
});

expect(mockedGetDomainAndOrganization).toHaveBeenCalledWith(req);
expect(mockedGetOrganization).toHaveBeenCalledWith(req);
expect(portalContextService.getContextValues).toHaveBeenCalledWith(req);
});
});
11 changes: 7 additions & 4 deletions src/portal-options/pm-request-context-provider.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -9,19 +9,22 @@ export interface RequestContext extends Record<string, any> {
organization: string;
crdGatewayApiUrl?: string;
isSubDomain: boolean;
isLocalhost: boolean;
}

@Injectable()
export class PMRequestContextProvider implements RequestContextProvider {
constructor(private pmPortalContextService: PMPortalContextService) {}

async getContextValues(request: Request): Promise<RequestContext> {
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,
isLocalhost: request.hostname.includes('localhost'),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ 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,
ServiceProviderService,
} from '@openmfp/portal-server-lib';
import { GraphQLClient } from 'graphql-request';

@Injectable()
export class ContentConfigurationServiceProvidersService
implements ServiceProviderService
{
Expand All @@ -22,7 +24,7 @@ export class ContentConfigurationServiceProvidersService
throw new Error('Token is required');
}

if (!context.isSubDomain) {
if (!context.isSubDomain && !context.isLocalhost) {
return welcomeNodeConfig;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class KubernetesServiceProvidersService
throw new Error('Token is required');
}

if (!context.isSubDomain) {
if (!context.isSubDomain && !context.isLocalhost) {
return welcomeNodeConfig;
}

Expand Down
54 changes: 14 additions & 40 deletions src/portal-options/utils/domain.spec.ts
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand All @@ -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');
});
});
Loading