diff --git a/.gitignore b/.gitignore index f12e71e..dbe99bc 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,5 @@ pids # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +/yalc.lock +/.yalc/ diff --git a/jest.config.ts b/jest.config.ts index 20080ec..576fce8 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -9,15 +9,17 @@ module.exports = { reporters: ['default'], coverageThreshold: { global: { - branches: 78, - functions: 80, + branches: 80, + functions: 95, lines: 95, - statements: -10, + statements: -3, }, }, coveragePathIgnorePatterns: ['/node_modules/', '/integration-tests/'], coverageDirectory: '../test-run-reports/coverage/unit', - transformIgnorePatterns: ['/node_modules/(?!(@openmfp/portal-server-lib|graphql-request)/)'], + transformIgnorePatterns: [ + '/node_modules/(?!(@openmfp/portal-server-lib|graphql-request)/)', + ], transform: { '^.+\\.(t|j)s$': [ 'ts-jest', diff --git a/package-lock.json b/package-lock.json index 995fc49..6fe54ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@nestjs/axios": ">=3.0.0", "@nestjs/common": ">=10.0.0", "@nestjs/serve-static": ">=4.0.0", + "@openmfp/portal-server-lib": ">=0.158.1", "axios": ">=1.7.7", "express": ">=4.21.1", "rxjs": ">=7.8.1" @@ -50,7 +51,7 @@ "@nestjs/core": ">=10.0.0", "@nestjs/platform-express": ">=10.0.0", "@nestjs/serve-static": ">=4.0.0", - "@openmfp/portal-server-lib": "^0.157.0", + "@openmfp/portal-server-lib": ">=0.157.0", "axios": ">=1.7.7", "cookie-parser": ">=1.4.7", "express": ">=4.21.1", @@ -2889,11 +2890,10 @@ } }, "node_modules/@openmfp/portal-server-lib": { - "version": "0.157.0", - "resolved": "https://npm.pkg.github.com/download/@openmfp/portal-server-lib/0.157.0/d8ca4d0164521a9e688522bb671bddd90a53a172", - "integrity": "sha512-sWVh/Lc/cTlT+e3v2Ejn4Ltn3q6im1Dxjz5LQ5hVBeNUQqhCRvDUj71tusUbQ7OHM44Cg27482YIgioFHgap5Q==", + "version": "0.158.1", + "resolved": "https://npm.pkg.github.com/download/@openmfp/portal-server-lib/0.158.1/16207cde0776006ae182ba97fcc1d2a81a80e510", + "integrity": "sha512-9AoOl13YXM3tYzQRlHAgGbeu2VJhuyErX/zmftifSxkrK2DkInpNuxwFJ0IFyttqY3iRUfPAEnmz6SQHnWjfXQ==", "license": "ISC", - "peer": true, "dependencies": { "@nestjs/axios": ">=3.0.0", "@nestjs/common": ">=10.0.0", @@ -3621,8 +3621,7 @@ "version": "13.15.2", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", "integrity": "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/yargs": { "version": "17.0.33", @@ -5347,15 +5346,13 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/class-validator": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", "license": "MIT", - "peer": true, "dependencies": { "@types/validator": "^13.11.8", "libphonenumber-js": "^1.11.1", @@ -8651,7 +8648,6 @@ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -8716,8 +8712,7 @@ "version": "1.12.13", "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.13.tgz", "integrity": "sha512-QZXnR/OGiDcBjF4hGk0wwVrPcZvbSSyzlvkjXv5LFfktj7O2VZDrt4Xs8SgR/vOFco+qk1i8J43ikMXZoTrtPw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lines-and-columns": { "version": "1.2.4", @@ -11965,7 +11960,6 @@ "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", "license": "MIT", - "peer": true, "engines": { "node": ">= 0.10" } diff --git a/package.json b/package.json index 7312d23..84e2d94 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,10 @@ "import": "./dist/portal-options/index.js", "types": "./dist/portal-options/index.d.ts" }, + "./services": { + "import": "./dist/services/index.js", + "types": "./dist/services/index.d.ts" + }, ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" @@ -18,6 +22,7 @@ }, "scripts": { "build": "nest build", + "build:watch": "mkdirp dist && nodemon --ignore dist --ext js,yml,yaml,ts,html,css,scss,json,md --exec \"rimraf dist && npm run build && yalc publish --push --sig\"", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", @@ -43,6 +48,7 @@ "@nestjs/axios": ">=3.0.0", "@nestjs/common": ">=10.0.0", "@nestjs/serve-static": ">=4.0.0", + "@openmfp/portal-server-lib": ">=0.158.1", "axios": ">=1.7.7", "express": ">=4.21.1", "rxjs": ">=7.8.1" @@ -73,5 +79,6 @@ "tsconfig-paths": "4.2.0", "typescript": "5.8.3", "typescript-eslint": "^8.0.0" - } + }, + "type": "module" } diff --git a/src/portal-options/entity-context-provider/account-entity-context-provider.service.ts b/src/portal-options/account-entity-context-provider.service.ts similarity index 100% rename from src/portal-options/entity-context-provider/account-entity-context-provider.service.ts rename to src/portal-options/account-entity-context-provider.service.ts diff --git a/src/portal-options/service-providers/iam/auth-callback-provider.spec.ts b/src/portal-options/auth-callback-provider.spec.ts similarity index 93% rename from src/portal-options/service-providers/iam/auth-callback-provider.spec.ts rename to src/portal-options/auth-callback-provider.spec.ts index 4f31cc6..8c65ff0 100644 --- a/src/portal-options/service-providers/iam/auth-callback-provider.spec.ts +++ b/src/portal-options/auth-callback-provider.spec.ts @@ -1,5 +1,5 @@ -import { AuthCallbackProvider } from './auth-callback-provider'; -import { IAMGraphQlService } from './iam-graphql.service'; +import { AuthCallbackProvider } from './auth-callback-provider.js'; +import { IAMGraphQlService } from './services/iam-graphql.service.js'; import { Test, TestingModule } from '@nestjs/testing'; import type { AuthTokenData } from '@openmfp/portal-server-lib'; import type { Request, Response } from 'express'; diff --git a/src/portal-options/service-providers/iam/auth-callback-provider.ts b/src/portal-options/auth-callback-provider.ts similarity index 91% rename from src/portal-options/service-providers/iam/auth-callback-provider.ts rename to src/portal-options/auth-callback-provider.ts index 60a1990..bd3f8d7 100644 --- a/src/portal-options/service-providers/iam/auth-callback-provider.ts +++ b/src/portal-options/auth-callback-provider.ts @@ -1,4 +1,4 @@ -import { IAMGraphQlService } from './iam-graphql.service.js'; +import { IAMGraphQlService } from './services/iam-graphql.service.js'; import { Injectable, Logger } from '@nestjs/common'; import { AuthCallback, AuthTokenData } from '@openmfp/portal-server-lib'; import { Request, Response } from 'express'; diff --git a/src/portal-options/auth-config-provider.spec.ts b/src/portal-options/auth-config-provider.spec.ts new file mode 100644 index 0000000..3e7faa2 --- /dev/null +++ b/src/portal-options/auth-config-provider.spec.ts @@ -0,0 +1,91 @@ +import { PMAuthConfigProvider } from './auth-config-provider.js'; +import { HttpException } from '@nestjs/common'; +import { + DiscoveryService, + EnvAuthConfigService, +} from '@openmfp/portal-server-lib'; +import type { Request } from 'express'; +import { mock } from 'jest-mock-extended'; + +describe('PMAuthConfigProvider', () => { + let provider: PMAuthConfigProvider; + let discoveryService: jest.Mocked; + let envAuthConfigService: jest.Mocked; + + beforeEach(() => { + discoveryService = mock(); + envAuthConfigService = mock(); + provider = new PMAuthConfigProvider(discoveryService, envAuthConfigService); + jest.resetModules(); + process.env = { + AUTH_SERVER_URL_DEFAULT: 'authUrl', + TOKEN_URL_DEFAULT: 'tokenUrl', + BASE_DOMAINS_DEFAULT: 'example.com', + OIDC_CLIENT_ID_DEFAULT: 'client123', + OIDC_CLIENT_SECRET_DEFAULT: 'secret123', + }; + }); + + it('should delegate to EnvAuthConfigService if available', async () => { + const req = { hostname: 'foo.example.com' } as Request; + const expected = { + idpName: 'idp', + baseDomain: 'example.com', + oauthServerUrl: 'url', + oauthTokenUrl: 'token', + clientId: 'cid', + clientSecret: 'sec', + }; + envAuthConfigService.getAuthConfig.mockResolvedValue(expected); + + const result = await provider.getAuthConfig(req); + + expect(result).toEqual(expected); + }); + + it('should fall back to default configuration if EnvAuthConfigService throws', async () => { + const req = { hostname: 'foo.example.com' } as Request; + envAuthConfigService.getAuthConfig.mockRejectedValue(new Error('fail')); + discoveryService.getOIDC.mockResolvedValue({ + authorization_endpoint: 'authUrl', + token_endpoint: 'tokenUrl', + }); + + const result = await provider.getAuthConfig(req); + + expect(result).toMatchObject({ + baseDomain: 'example.com', + oauthServerUrl: 'authUrl', + oauthTokenUrl: 'tokenUrl', + clientId: 'client123', + clientSecret: 'secret123', + }); + }); + + it('should throw if default configuration incomplete', async () => { + const req = { hostname: 'foo.example.com' } as Request; + envAuthConfigService.getAuthConfig.mockRejectedValue(new Error('fail')); + discoveryService.getOIDC.mockResolvedValue(null); + process.env = {}; + + 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 = provider.getDomain(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 = provider.getDomain(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 new file mode 100644 index 0000000..50c7306 --- /dev/null +++ b/src/portal-options/auth-config-provider.ts @@ -0,0 +1,75 @@ +import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { + AuthConfigService, + DiscoveryService, + EnvAuthConfigService, + ServerAuthVariables, +} from '@openmfp/portal-server-lib'; +import type { Request } from 'express'; + +@Injectable() +export class PMAuthConfigProvider implements AuthConfigService { + private logger: Logger = new Logger(PMAuthConfigProvider.name); + + constructor( + private discoveryService: DiscoveryService, + private envEuthConfigService: EnvAuthConfigService, + ) {} + + async getAuthConfig(request: Request): Promise { + try { + return await this.envEuthConfigService.getAuthConfig(request); + } catch { + this.logger.debug( + 'Failed to retrieve auth config from environment variables based on provided IDP.', + ); + } + + this.logger.debug('Resolving auth config from default configuration.'); + + const oidc = await this.discoveryService.getOIDC('DEFAULT'); + const oauthServerUrl = + oidc?.authorization_endpoint ?? process.env['AUTH_SERVER_URL_DEFAULT']; + const oauthTokenUrl = + oidc?.token_endpoint ?? process.env['TOKEN_URL_DEFAULT']; + + const baseDomain = process.env['BASE_DOMAINS_DEFAULT']; + const clientId = process.env['OIDC_CLIENT_ID_DEFAULT']; + const clientSecretEnvVar = 'OIDC_CLIENT_SECRET_DEFAULT'; + const clientSecret = process.env[clientSecretEnvVar]; + + if (!oauthServerUrl || !oauthTokenUrl || !clientId || !clientSecret) { + const hasClientSecret = !!clientSecret; + throw new HttpException( + { + message: 'Default auth configuration incomplete.', + error: `The default properly configured. oauthServerUrl: '${oauthServerUrl}' oauthTokenUrl: '${oauthTokenUrl}' clientId: '${clientId}', has client secret (${clientSecretEnvVar}): ${String( + hasClientSecret, + )}`, + statusCode: HttpStatus.NOT_FOUND, + }, + HttpStatus.NOT_FOUND, + ); + } + + const subDomain = request.hostname.split('.')[0]; + return { + idpName: request.hostname === baseDomain ? clientId : subDomain, + baseDomain, + oauthServerUrl, + clientId, + clientSecret, + oauthTokenUrl, + }; + } + + getDomain(request: Request): { organization?: string; baseDomain?: 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, + }; + } +} diff --git a/src/portal-options/index.ts b/src/portal-options/index.ts index b34589c..e6e67ed 100644 --- a/src/portal-options/index.ts +++ b/src/portal-options/index.ts @@ -1,6 +1,7 @@ -export * from './entity-context-provider/account-entity-context-provider.service.js'; -export * from './portal-context-provider/openmfp-portal-context.service.js'; -export * from './request-context-provider/openmfp-request-context-provider.js'; +export * from './account-entity-context-provider.service.js'; +export * from './openmfp-portal-context.service.js'; +export * from './openmfp-request-context-provider.js'; +export * from './auth-config-provider.js'; export * from './service-providers/content-configuration-service-providers.service.js'; -export * from './service-providers/iam/auth-callback-provider.js'; -export * from './service-providers/iam/iam-graphql.service.js'; +export * from './auth-callback-provider.js'; +export * from './services/iam-graphql.service.js'; diff --git a/src/portal-options/portal-context-provider/openmfp-portal-context.service.spec.ts b/src/portal-options/openmfp-portal-context.service.spec.ts similarity index 79% rename from src/portal-options/portal-context-provider/openmfp-portal-context.service.spec.ts rename to src/portal-options/openmfp-portal-context.service.spec.ts index 3c1b10e..9c1389a 100644 --- a/src/portal-options/portal-context-provider/openmfp-portal-context.service.spec.ts +++ b/src/portal-options/openmfp-portal-context.service.spec.ts @@ -1,24 +1,23 @@ +import { PMAuthConfigProvider } from './auth-config-provider.js'; import { OpenmfpPortalContextService } from './openmfp-portal-context.service.js'; import { Test, TestingModule } from '@nestjs/testing'; -import { EnvService } from '@openmfp/portal-server-lib'; import { Request } from 'express'; +import { mock } from 'jest-mock-extended'; describe('OpenmfpPortalContextService', () => { let service: OpenmfpPortalContextService; - let envService: jest.Mocked; + let pmAuthConfigProviderMock: jest.Mocked; let mockRequest: any; beforeEach(async () => { - const envServiceMock = { - getDomain: jest.fn(), - }; + pmAuthConfigProviderMock = mock(); const module: TestingModule = await Test.createTestingModule({ providers: [ OpenmfpPortalContextService, { - provide: EnvService, - useValue: envServiceMock, + provide: PMAuthConfigProvider, + useValue: pmAuthConfigProviderMock, }, ], }).compile(); @@ -26,8 +25,6 @@ describe('OpenmfpPortalContextService', () => { service = module.get( OpenmfpPortalContextService, ); - envService = module.get(EnvService); - mockRequest = { hostname: 'test.example.com', }; @@ -44,9 +41,9 @@ describe('OpenmfpPortalContextService', () => { }); it('should return empty context when no environment variables match prefix', async () => { - envService.getDomain.mockReturnValue({ - domain: 'example.com', - idpName: 'test-org', + pmAuthConfigProviderMock.getDomain.mockReturnValue({ + baseDomain: 'example.com', + organization: 'test-org', }); const result = await service.getContextValues(mockRequest as Request); @@ -60,9 +57,9 @@ describe('OpenmfpPortalContextService', () => { process.env.OTHER_ENV_VAR = 'should-be-ignored'; try { - envService.getDomain.mockReturnValue({ - domain: 'example.com', - idpName: 'test-org', + pmAuthConfigProviderMock.getDomain.mockReturnValue({ + baseDomain: 'example.com', + organization: 'test-org', }); const result = await service.getContextValues(mockRequest as Request); @@ -83,9 +80,9 @@ describe('OpenmfpPortalContextService', () => { process.env.OPENMFP_PORTAL_CONTEXT_MULTIPLE_SNAKE_CASE_KEYS = 'value2'; try { - envService.getDomain.mockReturnValue({ - domain: 'example.com', - idpName: 'test-org', + pmAuthConfigProviderMock.getDomain.mockReturnValue({ + baseDomain: 'example.com', + organization: 'test-org', }); const result = await service.getContextValues(mockRequest as Request); @@ -105,9 +102,9 @@ describe('OpenmfpPortalContextService', () => { 'https://${org-subdomain}api.example.com/${org-name}/graphql'; try { - envService.getDomain.mockReturnValue({ - domain: 'example.com', - idpName: 'test-org', + pmAuthConfigProviderMock.getDomain.mockReturnValue({ + baseDomain: 'example.com', + organization: 'test-org', }); mockRequest.hostname = 'subdomain.example.com'; @@ -127,9 +124,9 @@ describe('OpenmfpPortalContextService', () => { 'https://${org-subdomain}api.example.com/${org-name}/graphql'; try { - envService.getDomain.mockReturnValue({ - domain: 'example.com', - idpName: 'test-org', + pmAuthConfigProviderMock.getDomain.mockReturnValue({ + baseDomain: 'example.com', + organization: 'test-org', }); mockRequest.hostname = 'example.com'; @@ -150,9 +147,9 @@ describe('OpenmfpPortalContextService', () => { process.env.OPENMFP_PORTAL_CONTEXT_VALID_KEY = 'valid-value'; try { - envService.getDomain.mockReturnValue({ - domain: 'example.com', - idpName: 'test-org', + pmAuthConfigProviderMock.getDomain.mockReturnValue({ + baseDomain: 'example.com', + organization: 'test-org', }); const result = await service.getContextValues(mockRequest as Request); @@ -171,9 +168,9 @@ describe('OpenmfpPortalContextService', () => { process.env.OPENMFP_PORTAL_CONTEXT_OTHER_KEY = 'value'; try { - envService.getDomain.mockReturnValue({ - domain: 'example.com', - idpName: 'test-org', + pmAuthConfigProviderMock.getDomain.mockReturnValue({ + baseDomain: 'example.com', + organization: 'test-org', }); const result = await service.getContextValues(mockRequest as Request); diff --git a/src/portal-options/portal-context-provider/openmfp-portal-context.service.ts b/src/portal-options/openmfp-portal-context.service.ts similarity index 75% rename from src/portal-options/portal-context-provider/openmfp-portal-context.service.ts rename to src/portal-options/openmfp-portal-context.service.ts index b214c48..7a1f0fd 100644 --- a/src/portal-options/portal-context-provider/openmfp-portal-context.service.ts +++ b/src/portal-options/openmfp-portal-context.service.ts @@ -1,5 +1,6 @@ +import { PMAuthConfigProvider } from './auth-config-provider.js'; import { Injectable } from '@nestjs/common'; -import { EnvService, PortalContextProvider } from '@openmfp/portal-server-lib'; +import { PortalContextProvider } from '@openmfp/portal-server-lib'; import type { Request } from 'express'; import process from 'node:process'; @@ -7,13 +8,13 @@ import process from 'node:process'; export class OpenmfpPortalContextService implements PortalContextProvider { private readonly openmfpPortalContext = 'OPENMFP_PORTAL_CONTEXT_'; - constructor(private envService: EnvService) {} + constructor(private authConfigProvider: PMAuthConfigProvider) {} getContextValues(request: Request): Promise> { const portalContext: Record = {}; const keys = Object.keys(process.env).filter((item) => - item.startsWith(this.openmfpPortalContext) + item.startsWith(this.openmfpPortalContext), ); keys.forEach((key) => { const keyName = key.substring(this.openmfpPortalContext.length).trim(); @@ -29,13 +30,14 @@ export class OpenmfpPortalContextService implements PortalContextProvider { private processGraphQLGatewayApiUrl( request: Request, - portalContext: Record + portalContext: Record, ): void { - const org = this.envService.getDomain(request); - const subDomain = request.hostname === org.domain ? '' : `${org.idpName}.`; + const org = this.authConfigProvider.getDomain(request); + const subDomain = + request.hostname === org.baseDomain ? '' : `${org.organization}.`; portalContext.crdGatewayApiUrl = portalContext.crdGatewayApiUrl ?.replace('${org-subdomain}', subDomain) - .replace('${org-name}', `${org.idpName}`); + .replace('${org-name}', org.organization); } private toCamelCase(text: string): string { diff --git a/src/portal-options/request-context-provider/openmfp-request-context-provider.spec.ts b/src/portal-options/openmfp-request-context-provider.spec.ts similarity index 68% rename from src/portal-options/request-context-provider/openmfp-request-context-provider.spec.ts rename to src/portal-options/openmfp-request-context-provider.spec.ts index d53866d..55021b4 100644 --- a/src/portal-options/request-context-provider/openmfp-request-context-provider.spec.ts +++ b/src/portal-options/openmfp-request-context-provider.spec.ts @@ -1,19 +1,21 @@ -import { OpenmfpPortalContextService } from '../portal-context-provider/openmfp-portal-context.service'; -import { RequestContextProviderImpl } from './openmfp-request-context-provider'; -import { EnvService } from '@openmfp/portal-server-lib'; +import { PMAuthConfigProvider } from './auth-config-provider.js'; +import { OpenmfpPortalContextService } from './openmfp-portal-context.service.js'; +import { RequestContextProviderImpl } from './openmfp-request-context-provider.js'; import type { Request } from 'express'; import { mock } from 'jest-mock-extended'; describe('RequestContextProviderImpl', () => { let provider: RequestContextProviderImpl; - const envService = mock(); + const pmAuthConfigProviderMock = mock(); const portalContext = mock(); beforeEach(() => { jest.resetAllMocks(); - (envService.getDomain as unknown as jest.Mock).mockReturnValue({ - idpName: 'org1', - domain: 'org1.example.com', + ( + pmAuthConfigProviderMock.getDomain as unknown as jest.Mock + ).mockReturnValue({ + organization: 'org1', + baseDomain: 'org1.example.com', }); (portalContext.getContextValues as unknown as jest.Mock).mockResolvedValue({ crdGatewayApiUrl: 'http://gateway/graphql', @@ -21,8 +23,8 @@ describe('RequestContextProviderImpl', () => { }); provider = new RequestContextProviderImpl( - envService as any, - portalContext as any, + pmAuthConfigProviderMock, + portalContext, ); }); @@ -42,7 +44,7 @@ describe('RequestContextProviderImpl', () => { organization: 'org1', }); - expect(envService.getDomain).toHaveBeenCalledWith(req); + expect(pmAuthConfigProviderMock.getDomain).toHaveBeenCalledWith(req); expect(portalContext.getContextValues).toHaveBeenCalledWith(req); }); }); diff --git a/src/portal-options/request-context-provider/openmfp-request-context-provider.ts b/src/portal-options/openmfp-request-context-provider.ts similarity index 53% rename from src/portal-options/request-context-provider/openmfp-request-context-provider.ts rename to src/portal-options/openmfp-request-context-provider.ts index 32b037c..981bb56 100644 --- a/src/portal-options/request-context-provider/openmfp-request-context-provider.ts +++ b/src/portal-options/openmfp-request-context-provider.ts @@ -1,26 +1,30 @@ +import { PMAuthConfigProvider } from './auth-config-provider.js'; +import { OpenmfpPortalContextService } from './openmfp-portal-context.service.js'; import { Injectable } from '@nestjs/common'; -import { EnvService, RequestContextProvider } from '@openmfp/portal-server-lib'; +import { RequestContextProvider } from '@openmfp/portal-server-lib'; import type { Request } from 'express'; -import { OpenmfpPortalContextService } from '../portal-context-provider/openmfp-portal-context.service.js'; export interface RequestContext extends Record { account?: string; - organization?: string; + organization: string; crdGatewayApiUrl?: string; + isSubDomain: boolean; } @Injectable() export class RequestContextProviderImpl implements RequestContextProvider { constructor( - private envService: EnvService, - private openmfpPortalContextService: OpenmfpPortalContextService + private authConfigProvider: PMAuthConfigProvider, + private openmfpPortalContextService: OpenmfpPortalContextService, ) {} async getContextValues(request: Request): Promise { + const domainData = this.authConfigProvider.getDomain(request); return { ...request.query, ...(await this.openmfpPortalContextService.getContextValues(request)), - organization: this.envService.getDomain(request).idpName, + organization: domainData.organization, + isSubDomain: request.hostname !== domainData.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 abbe20c..b77020e 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 @@ -1,4 +1,4 @@ -import { RequestContext } from '../request-context-provider/openmfp-request-context-provider.js'; +import { RequestContext } from '../openmfp-request-context-provider.js'; import { ContentConfigurationServiceProvidersService } from './content-configuration-service-providers.service.js'; import { GraphQLClient } from 'graphql-request'; @@ -23,6 +23,7 @@ describe('ContentConfigurationServiceProvidersService', () => { mockClient = new GraphQLClient('') as any; (GraphQLClient as jest.Mock).mockReturnValue(mockClient); context = { + isSubDomain: true, organization: 'org1', crdGatewayApiUrl: 'http://example.com/kubernetes-graphql-gateway/root/graphql', 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 360319b..ed266b9 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 @@ -1,32 +1,13 @@ -import { RequestContext } from '../request-context-provider/openmfp-request-context-provider.js'; +import { RequestContext } from '../openmfp-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 { ContentConfiguration, ServiceProviderResponse, ServiceProviderService, } from '@openmfp/portal-server-lib'; -import { GraphQLClient, gql } from 'graphql-request'; - -export const contentConfigurationsQuery = gql` - query { - ui_platform_mesh_io { - ContentConfigurations { - metadata { - name - labels - } - spec { - remoteConfiguration { - url - } - } - status { - configurationResult - } - } - } - } -`; +import { GraphQLClient } from 'graphql-request'; export class ContentConfigurationServiceProvidersService implements ServiceProviderService @@ -41,6 +22,10 @@ export class ContentConfigurationServiceProvidersService throw new Error('Token is required'); } + if (!context.isSubDomain) { + return welcomeNodeConfig; + } + if (!context?.organization) { throw new Error('Context with organization is required'); } diff --git a/src/portal-options/service-providers/contentconfigurations-query.ts b/src/portal-options/service-providers/contentconfigurations-query.ts new file mode 100644 index 0000000..4d913d1 --- /dev/null +++ b/src/portal-options/service-providers/contentconfigurations-query.ts @@ -0,0 +1,22 @@ +import { gql } from 'graphql-request'; + +export const contentConfigurationsQuery = gql` + query { + ui_platform_mesh_io { + ContentConfigurations { + metadata { + name + labels + } + spec { + remoteConfiguration { + url + } + } + status { + configurationResult + } + } + } + } +`; diff --git a/src/portal-options/service-providers/kubernetes-service-providers.service.spec.ts b/src/portal-options/service-providers/kubernetes-service-providers.service.spec.ts index f0dcef9..42415c7 100644 --- a/src/portal-options/service-providers/kubernetes-service-providers.service.spec.ts +++ b/src/portal-options/service-providers/kubernetes-service-providers.service.spec.ts @@ -1,4 +1,4 @@ -import { KubernetesServiceProvidersService } from './kubernetes-service-providers.service'; +import { KubernetesServiceProvidersService } from './kubernetes-service-providers.service.js'; const listClusterCustomObject = jest.fn(); diff --git a/src/portal-options/service-providers/models/contentconfigurations.ts b/src/portal-options/service-providers/models/contentconfigurations.ts index 6388c82..ffd38c0 100644 --- a/src/portal-options/service-providers/models/contentconfigurations.ts +++ b/src/portal-options/service-providers/models/contentconfigurations.ts @@ -10,4 +10,4 @@ export interface ContentConfigurationResponse { metadata: { name: string; labels?: Record; }; spec: { remoteConfiguration?: { url?: string; }; }; status: { configurationResult?: string; }; -} \ No newline at end of file +} diff --git a/src/portal-options/service-providers/models/welcome-node-config.ts b/src/portal-options/service-providers/models/welcome-node-config.ts new file mode 100644 index 0000000..3b1a5c0 --- /dev/null +++ b/src/portal-options/service-providers/models/welcome-node-config.ts @@ -0,0 +1,35 @@ +import { ServiceProviderResponse } from '@openmfp/portal-server-lib'; + +export const welcomeNodeConfig: ServiceProviderResponse = { + rawServiceProviders: [ + { + name: 'platform-mesh-system', + displayName: '', + creationTimestamp: '', + contentConfiguration: [ + { + name: 'platform-mesh-system', + creationTimestamp: '', + luigiConfigFragment: { + data: { + nodes: [ + { + entityType: 'global', + pathSegment: 'welcome', + hideFromNav: true, + hideSideNav: true, + order: 1, + url: '/assets/platform-mesh-portal-ui-wc.js#welcome-view', + webcomponent: { + selfRegistered: true, + }, + context: { kcpPath: 'root:orgs' }, + }, + ], + }, + }, + }, + ], + }, + ], +}; diff --git a/src/portal-options/service-providers/iam/iam-graphql.service.spec.ts b/src/portal-options/services/iam-graphql.service.spec.ts similarity index 89% rename from src/portal-options/service-providers/iam/iam-graphql.service.spec.ts rename to src/portal-options/services/iam-graphql.service.spec.ts index 5ff3fa1..5e18b8c 100644 --- a/src/portal-options/service-providers/iam/iam-graphql.service.spec.ts +++ b/src/portal-options/services/iam-graphql.service.spec.ts @@ -1,6 +1,6 @@ -import { RequestContextProviderImpl } from '../../request-context-provider/openmfp-request-context-provider'; -import { IAMGraphQlService } from './iam-graphql.service'; -import { MUTATION_LOGIN } from './queries'; +import { RequestContextProviderImpl } from '../openmfp-request-context-provider.js'; +import { IAMGraphQlService } from './iam-graphql.service.js'; +import { MUTATION_LOGIN } from './queries.js'; import { GraphQLClient } from 'graphql-request'; import { mock } from 'jest-mock-extended'; diff --git a/src/portal-options/service-providers/iam/iam-graphql.service.ts b/src/portal-options/services/iam-graphql.service.ts similarity index 80% rename from src/portal-options/service-providers/iam/iam-graphql.service.ts rename to src/portal-options/services/iam-graphql.service.ts index 71e2894..1252ab4 100644 --- a/src/portal-options/service-providers/iam/iam-graphql.service.ts +++ b/src/portal-options/services/iam-graphql.service.ts @@ -1,4 +1,4 @@ -import { RequestContextProviderImpl } from '../../request-context-provider/openmfp-request-context-provider'; +import { RequestContextProviderImpl } from '../openmfp-request-context-provider.js'; import { MUTATION_LOGIN } from './queries.js'; import { Injectable } from '@nestjs/common'; import type { Request } from 'express'; @@ -19,7 +19,7 @@ export class IAMGraphQlService { }); try { - const response = await client.request(MUTATION_LOGIN); + await client.request(MUTATION_LOGIN); } catch (e) { console.error(e); } diff --git a/src/portal-options/service-providers/iam/queries.ts b/src/portal-options/services/queries.ts similarity index 100% rename from src/portal-options/service-providers/iam/queries.ts rename to src/portal-options/services/queries.ts