diff --git a/.github/workflows/mainCI.yml b/.github/workflows/mainCI.yml index df7d88e0e..e8a51624e 100644 --- a/.github/workflows/mainCI.yml +++ b/.github/workflows/mainCI.yml @@ -18,10 +18,13 @@ jobs: security-events: write steps: - - name: Use Node.js 20.x + - name: Checkout code uses: actions/checkout@v2 + + - name: Setup Node.js 20 + uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 20 - name: Install run: npm install diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7793ba33f..8f912a981 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,11 @@ jobs: - name: Checkout code uses: actions/checkout@v3 + - name: Setup Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Install run: npm install diff --git a/package-lock.json b/package-lock.json index bfddb08da..719ea56ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "knockout-mapping": "^2.6.0", "knockout.validation": "^2.0.4", "liquidjs": "^10.9.2", + "lodash": "4.18.1", "lunr": "^2.3.9", "mime": "^3.0.0", "moment": "^2.29.4", @@ -10294,9 +10295,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.get": { diff --git a/package.json b/package.json index 6aebf0a78..4a10dea85 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "build-publisher": "webpack --config webpack.publisher.js", "build-runtime": "webpack --config webpack.runtime.js", "build-function": "webpack --config webpack.function.js", - "test": "node node_modules/mocha/bin/_mocha -r mocha.js src/**/*.spec.ts", + "test": "node node_modules/mocha/bin/_mocha -r mocha.js --exit src/**/*.spec.ts", "deploy-function": "npm run build-function && cd dist/function && func azure functionapp publish < function app name >", "publish": "webpack --config webpack.publisher.js && node dist/publisher/index.js && npm run serve-website", "serve-website": "webpack serve --open --static ./dist/website --no-stats", @@ -116,6 +116,7 @@ "knockout-mapping": "^2.6.0", "knockout.validation": "^2.0.4", "liquidjs": "^10.9.2", + "lodash": "4.18.1", "lunr": "^2.3.9", "mime": "^3.0.0", "moment": "^2.29.4", diff --git a/src/authentication/authenticatorResolver.ts b/src/authentication/authenticatorResolver.ts index d0da4566d..9c9d68a8f 100644 --- a/src/authentication/authenticatorResolver.ts +++ b/src/authentication/authenticatorResolver.ts @@ -6,6 +6,8 @@ import { ArmAuthenticator } from "./armAuthenticator"; import { IAuthenticator } from "./IAuthenticator"; import { IEditorSettings } from "./IEditorSettings"; +declare const ARM_TOKEN: string | undefined; + export class AuthenticatorResolver { private loadPromise: Promise; diff --git a/src/authentication/staticAuthenticator.ts b/src/authentication/staticAuthenticator.ts index 3d6a0a5c6..d349f091b 100644 --- a/src/authentication/staticAuthenticator.ts +++ b/src/authentication/staticAuthenticator.ts @@ -1,5 +1,7 @@ import { IAuthenticator, AccessToken } from "."; +declare const ARM_TOKEN: string | undefined; + /** * Static implementation of the IAuthenticator interface to mimic actual authentication in publish time. */ @@ -11,7 +13,7 @@ export class StaticAuthenticator implements IAuthenticator { * The ARM token injected acquired in build-time. It's used in on local development only. * TODO: Static authenticator is used in production publishing, therefore it's safer to introduce dedicated implementation. */ - if (!ARM_TOKEN) { + if (typeof ARM_TOKEN === "undefined" || !ARM_TOKEN) { return; } diff --git a/src/clients/mapiClient.spec.ts b/src/clients/mapiClient.spec.ts index cbd98f24f..07062a43c 100644 --- a/src/clients/mapiClient.spec.ts +++ b/src/clients/mapiClient.spec.ts @@ -21,7 +21,7 @@ describe("Mapi Client", async () => { global.sessionStorage = { _values: new Map(), - length: global.sessionStorage._values.size, + get length() { return this._values.size; }, key: (index: number) => { return null; }, getItem: (key: string) => { return global.sessionStorage._values.get(key); }, setItem: (key: string, value: string) => { global.sessionStorage._values.set(key, value); }, @@ -29,6 +29,10 @@ describe("Mapi Client", async () => { clear: () => { global.sessionStorage._values.clear(); } } + // Pre-seed a valid non-expired SAS token so ArmAuthenticator.getAccessToken() returns from + // sessionStorage cache and never calls authenticate(), which relies on browser-only `location`. + global.sessionStorage.setItem("armAccessToken", createMockToken()); + const settingsProvider = new StaticSettingsProvider({ managementApiUrl: "https://contoso.management.azure-api.net", backendUrl: "https://contoso.developer.azure-api.net", diff --git a/src/clients/mapiClient.ts b/src/clients/mapiClient.ts index e0925f607..454961f37 100644 --- a/src/clients/mapiClient.ts +++ b/src/clients/mapiClient.ts @@ -7,6 +7,7 @@ import * as Constants from "../constants"; import ApiClient from "./apiClient"; import { IRetryStrategy } from "./retryStrategy/retryStrategy"; import { SettingNames } from "../constants"; +import { Utils } from "../utils"; export class MapiClient extends ApiClient { constructor( @@ -22,25 +23,29 @@ export class MapiClient extends ApiClient { protected override async setBaseUrl() { const settings = await this.settingsProvider.getSettings(); - + + const managementApiUrl = settings[Constants.SettingNames.managementApiUrl]; + if (managementApiUrl) { + this.baseUrl = managementApiUrl; + return; + } + const serviceName = settings[Constants.SettingNames.serviceName]; const subscriptionId = settings[Constants.SettingNames.subscriptionId]; const resourceGroupName = settings[Constants.SettingNames.resourceGroupName]; - if (!serviceName) { - throw new Error("Service name setting is missing."); + if (serviceName && subscriptionId && resourceGroupName) { + this.baseUrl = `https://management.azure.com/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.ApiManagement/service/${serviceName}`; + return; } - if (!subscriptionId) { - throw new Error("Subscription ID setting is missing."); + const backendUrl = settings[Constants.SettingNames.backendUrl]; + if (backendUrl) { + this.baseUrl = Utils.getBaseUrlWithMapiSuffix(backendUrl); + return; } - if (!resourceGroupName) { - throw new Error("Resource Group name setting is missing."); - } - - this.baseUrl = `https://management.azure.com/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.ApiManagement/service/${serviceName}`; - + throw new Error("Unable to determine management API URL: no valid configuration found (managementApiUrl, serviceName/subscriptionId/resourceGroupName, or backendUrl must be set)."); } public async getTenantArmUriAsync(): Promise { diff --git a/src/components/products/product-subscribe/ko/runtime/product-subscribe.ts b/src/components/products/product-subscribe/ko/runtime/product-subscribe.ts index 1554a2caf..fd77abc34 100644 --- a/src/components/products/product-subscribe/ko/runtime/product-subscribe.ts +++ b/src/components/products/product-subscribe/ko/runtime/product-subscribe.ts @@ -81,7 +81,7 @@ export class ProductSubscribe { return; } - const product = await this.productService.getProduct(`products/${productName}`); + const product = await this.productService.getProduct(productName); if (!product) { return; diff --git a/src/components/products/product-subscribe/react/runtime/ProductSubscribeRuntime.tsx b/src/components/products/product-subscribe/react/runtime/ProductSubscribeRuntime.tsx index 3ada1b2ed..09b374456 100644 --- a/src/components/products/product-subscribe/react/runtime/ProductSubscribeRuntime.tsx +++ b/src/components/products/product-subscribe/react/runtime/ProductSubscribeRuntime.tsx @@ -27,7 +27,7 @@ type ProductSubscribeRuntimeFCProps = ProductSubscribeRuntimeProps & { const loadProduct = async (usersService: UsersService, delegationService: IDelegationService, productService: ProductService, productName: string) => { const promises = [ await delegationService.isSubscriptionDelegationEnabled(), - await productService.getProduct(`products/${productName}`), + await productService.getProduct(productName), await usersService.getCurrentUserId(), ] as const; const [isDelegationEnabled, product, userId] = await Promise.all(promises); diff --git a/src/config.validate.json b/src/config.validate.json index b9a167789..e2ce2c081 100644 --- a/src/config.validate.json +++ b/src/config.validate.json @@ -1,7 +1,7 @@ { "environment": "validation", - "isLocalRun": false, - "root": "https://.developer.azure-api.net", + "isLocalRun": true, + "root": "http://localhost:8080", "managementUrl": "https://.management.azure-api.net", "accessToken": "SharedAccessSignature ...", "useArmAuth": false, diff --git a/src/startup.publish.ts b/src/startup.publish.ts index 0bb86c980..cf0a2d15c 100644 --- a/src/startup.publish.ts +++ b/src/startup.publish.ts @@ -20,7 +20,8 @@ if (process.env.NODE_ENV === staticDataEnvironment || process.env.NODE_ENV === m "environment": "publishing", "backendUrl": "https://contoso.developer.azure-api.net", "managementApiAccessToken": "SharedAccessSignature&1&", - "useHipCaptcha": false + "useHipCaptcha": false, + "managementApiUrl": "https://contoso.developer.azure-api.net/mapi" }); } else { diff --git a/webpack.build.js b/webpack.build.js index 79384463d..9da6b4c2d 100644 --- a/webpack.build.js +++ b/webpack.build.js @@ -1,7 +1,7 @@ const { merge } = require("webpack-merge"); const TerserPlugin = require("terser-webpack-plugin"); const { designerConfig, designerRuntimeConfig } = require("./webpack.designer.js"); -const { publisherConfig, publisherRuntimeConfig } = require("./webpack.publisher.js"); +const getPublisherConfigs = require("./webpack.publisher.js"); const productionConfig = { @@ -20,9 +20,13 @@ const productionConfig = { } } -module.exports = [ - designerConfig, - designerRuntimeConfig, - publisherConfig, - publisherRuntimeConfig -].map(x => merge(x, productionConfig)); \ No newline at end of file +module.exports = async () => { + const [publisherConfig, publisherRuntimeConfig] = await getPublisherConfigs(); + + return [ + designerConfig, + designerRuntimeConfig, + publisherConfig, + publisherRuntimeConfig + ].map(x => merge(x, productionConfig)); +}; \ No newline at end of file diff --git a/webpack.mockStaticData.js b/webpack.mockStaticData.js index c2c336b74..6df94062d 100644 --- a/webpack.mockStaticData.js +++ b/webpack.mockStaticData.js @@ -1,31 +1,26 @@ -const {mockStaticDataEnvironment, staticDataEnvironment} = require("./environmentConstants") +const { mockStaticDataEnvironment } = require("./environmentConstants"); const webpack = require("webpack"); -const publisherConfig = require("./webpack.publisher"); +const getPublisherConfigs = require("./webpack.publisher"); const CopyWebpackPlugin = require("copy-webpack-plugin"); - -let staticData = publisherConfig; - -staticData.publisherConfig.mode = "none"; -staticData.publisherConfig.plugins.push(new webpack.DefinePlugin({ - "process.env.NODE_ENV": JSON.stringify(mockStaticDataEnvironment) -})); - -staticData.publisherRuntimeConfig.mode = "none"; - -staticData.publisherConfig.mode = "none"; -staticData.publisherConfig.plugins.push(new CopyWebpackPlugin({ - patterns: [ - { from: `./src/config.validate.publish.json`, to: `config.json`, force: true }, - { from: `./src/config.validate.runtime.json`, to: `assets/config.json` , force: true}, - ] -})); - - -staticData.publisherRuntimeConfig.plugins.push(new webpack.DefinePlugin({ - "process.env.NODE_ENV": JSON.stringify(mockStaticDataEnvironment), -})); - -module.exports = { - default: [staticData.publisherConfig, staticData.publisherRuntimeConfig] -} \ No newline at end of file +module.exports = async () => { + const [publisherConfig, publisherRuntimeConfig] = await getPublisherConfigs({ skipArmToken: true }); + + publisherConfig.mode = "none"; + publisherConfig.plugins.push(new webpack.DefinePlugin({ + "process.env.NODE_ENV": JSON.stringify(mockStaticDataEnvironment) + })); + publisherConfig.plugins.push(new CopyWebpackPlugin({ + patterns: [ + { from: `./src/config.validate.publish.json`, to: `config.json`, force: true }, + { from: `./src/config.validate.runtime.json`, to: `assets/config.json`, force: true }, + ] + })); + + publisherRuntimeConfig.mode = "none"; + publisherRuntimeConfig.plugins.push(new webpack.DefinePlugin({ + "process.env.NODE_ENV": JSON.stringify(mockStaticDataEnvironment), + })); + + return [publisherConfig, publisherRuntimeConfig]; +}; \ No newline at end of file diff --git a/webpack.publisher.js b/webpack.publisher.js index 3ad82ef31..4a16aea1b 100644 --- a/webpack.publisher.js +++ b/webpack.publisher.js @@ -12,18 +12,22 @@ const publisherRuntimeConfig = merge(runtimeConfig, { output: { "path": path.resolve(__dirname, "dist/publisher/assets") } }); -async function generateWebpackConfig() { - const tokenOptions = {}; - - if (config.tenantId) { - console.log(`Using tenantId: ${config.tenantId}`); - tokenOptions.tenantId = config.tenantId; - } - if (config.clientId) { - console.log(`Using clientId: ${config.clientId}`); - tokenOptions.clientId = config.clientId; +async function generateWebpackConfig(skipArmToken = false) { + let armToken; + + if (!skipArmToken) { + const tokenOptions = {}; + + if (config.tenantId) { + console.log(`Using tenantId: ${config.tenantId}`); + tokenOptions.tenantId = config.tenantId; + } + if (config.clientId) { + console.log(`Using clientId: ${config.clientId}`); + tokenOptions.clientId = config.clientId; + } + armToken = await getArmToken(tokenOptions); } - const armToken = await getArmToken(tokenOptions); const publisherConfig = { mode: "development", @@ -110,7 +114,7 @@ async function generateWebpackConfig() { return publisherConfig; } -module.exports = async () => { - const publisherConfig = await generateWebpackConfig(); +module.exports = async (options = {}) => { + const publisherConfig = await generateWebpackConfig(options.skipArmToken ?? false); return [publisherConfig, publisherRuntimeConfig]; }; \ No newline at end of file diff --git a/webpack.staticData.js b/webpack.staticData.js index a78466a57..dd8b3fb9e 100644 --- a/webpack.staticData.js +++ b/webpack.staticData.js @@ -1,35 +1,32 @@ -const {staticDataEnvironment} = require("./environmentConstants") +const { staticDataEnvironment } = require("./environmentConstants"); const webpack = require("webpack"); -const publisherConfig = require("./webpack.publisher"); +const getPublisherConfigs = require("./webpack.publisher"); const CopyWebpackPlugin = require("copy-webpack-plugin"); +const staticDataCopyPatterns = [ + { from: `./templates/default-old.json`, to: "editors/templates/default.json" }, + { from: `./tests/mocks/defaultStaticData.json`, to: "tests/mocks/defaultStaticData.json" } +]; -let staticData = publisherConfig; +module.exports = async () => { + const [publisherConfig, publisherRuntimeConfig] = await getPublisherConfigs({ skipArmToken: true }); -staticData.publisherConfig.mode = "none"; -staticData.publisherConfig.plugins.push(new webpack.DefinePlugin({ - "process.env.NODE_ENV": JSON.stringify(staticDataEnvironment) -})); -staticData.publisherConfig.plugins.push(new CopyWebpackPlugin({ - patterns: [ - { from: `./templates/default-old.json`, to: "editors/templates/default.json" }, - { from: `./tests/mocks/defaultStaticData.json`, to: "tests/mocks/defaultStaticData.json" } - ] -})); + publisherConfig.mode = "none"; + publisherConfig.plugins.push(new webpack.DefinePlugin({ + "process.env.NODE_ENV": JSON.stringify(staticDataEnvironment) + })); + publisherConfig.plugins.push(new CopyWebpackPlugin({ + patterns: staticDataCopyPatterns + })); -staticData.publisherRuntimeConfig.mode = "none"; -staticData.publisherRuntimeConfig.plugins.push(new webpack.DefinePlugin({ - "process.env.NODE_ENV": JSON.stringify(staticDataEnvironment), - "process.env.ACCESS_TOKEN": "\"\"", -})); + publisherRuntimeConfig.mode = "none"; + publisherRuntimeConfig.plugins.push(new webpack.DefinePlugin({ + "process.env.NODE_ENV": JSON.stringify(staticDataEnvironment), + "process.env.ACCESS_TOKEN": "\"\"", + })); + publisherRuntimeConfig.plugins.push(new CopyWebpackPlugin({ + patterns: staticDataCopyPatterns + })); -staticData.publisherRuntimeConfig.plugins.push(new CopyWebpackPlugin({ - patterns: [ - { from: `./templates/default-old.json`, to: "editors/templates/default.json" }, - { from: `./tests/mocks/defaultStaticData.json`, to: "tests/mocks/defaultStaticData.json" } - ] -})); - -module.exports = { - default: [staticData.publisherConfig, staticData.publisherRuntimeConfig] -} \ No newline at end of file + return [publisherConfig, publisherRuntimeConfig]; +}; \ No newline at end of file