From e57da6a7a916f6967200697902cbb3286f88fc80 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Thu, 6 Aug 2026 14:09:18 +1000 Subject: [PATCH 1/6] Use updated uninav-react version --- __tests__/config/uninav.js | 35 +++++++++++++++++++++++++++++++++++ package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 __tests__/config/uninav.js diff --git a/__tests__/config/uninav.js b/__tests__/config/uninav.js new file mode 100644 index 000000000..7a51e9720 --- /dev/null +++ b/__tests__/config/uninav.js @@ -0,0 +1,35 @@ +/* eslint-env jest */ + +const { execFileSync } = require('child_process'); +const path = require('path'); + +const PROJECT_ROOT = path.resolve(__dirname, '../..'); + +test('UniNav components render in the plain Node production runtime', () => { + const script = ` + const React = require('react'); + const ReactDOMServer = require('react-dom/server'); + const { MarketingNavigation, ToolNavigation } = require('uninav-react'); + + const components = { MarketingNavigation, ToolNavigation }; + for (const [name, Component] of Object.entries(components)) { + if (typeof Component !== 'function') { + throw new TypeError(name + ' is not a React component'); + } + ReactDOMServer.renderToString(React.createElement(Component, { + currentLocation: '/', + toolName: 'Topcoder', + })); + } + `; + + expect(() => execFileSync(process.execPath, ['-e', script], { + cwd: PROJECT_ROOT, + env: { + ...process.env, + BABEL_ENV: 'production', + NODE_ENV: 'production', + }, + stdio: 'pipe', + })).not.toThrow(); +}); diff --git a/package-lock.json b/package-lock.json index 3aab0fc96..40b8c86cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -146,7 +146,7 @@ "topcoder-react-utils": "https://codeload.github.com/topcoder-platform/topcoder-react-utils/tar.gz/0fcf9a756a371e0ad633636ba050a7881d862cb8", "turndown": "^7.2.4", "ua-parser-js": "^1.0.41", - "uninav-react": "https://codeload.github.com/topcoder-platform/uninav-react/tar.gz/3d1c75e56bdba4140ce8341c854ce9a33a2d2c9b", + "uninav-react": "https://codeload.github.com/topcoder-platform/uninav-react/tar.gz/1f82f36c52dad1cccd4bff0982d3aa087ce7fd0f", "url-parse": "^1.4.1", "uuid": "^11.1.1", "valid-url": "^1.0.9", @@ -24558,9 +24558,9 @@ } }, "node_modules/uninav-react": { - "version": "0.0.2", - "resolved": "https://codeload.github.com/topcoder-platform/uninav-react/tar.gz/3d1c75e56bdba4140ce8341c854ce9a33a2d2c9b", - "integrity": "sha512-91kRuGYFpqdxEyb2hW+pZKivHMR/IzoC39g4zI6E73T44pDgjGz55sx62A9tqcYeQdKrOiXxrA91H/gzj7mRYA==", + "version": "0.0.3", + "resolved": "https://codeload.github.com/topcoder-platform/uninav-react/tar.gz/1f82f36c52dad1cccd4bff0982d3aa087ce7fd0f", + "integrity": "sha512-eOkw/43SH6FIUQTYALiaYxTGFqbfIFwDcbek02YPD/oxVftDU2FEVBZ+bgOE7BFnQql4IR6ruQ3efuEHa6fCHw==", "peerDependencies": { "react": "^16.4.1", "react-dom": "^16.4.1" diff --git a/package.json b/package.json index f326fb673..f2ed01547 100644 --- a/package.json +++ b/package.json @@ -173,7 +173,7 @@ "topcoder-react-ui-kit": "2.0.1", "topcoder-react-utils": "https://codeload.github.com/topcoder-platform/topcoder-react-utils/tar.gz/0fcf9a756a371e0ad633636ba050a7881d862cb8", "turndown": "^7.2.4", - "uninav-react": "https://codeload.github.com/topcoder-platform/uninav-react/tar.gz/3d1c75e56bdba4140ce8341c854ce9a33a2d2c9b", + "uninav-react": "https://codeload.github.com/topcoder-platform/uninav-react/tar.gz/1f82f36c52dad1cccd4bff0982d3aa087ce7fd0f", "ua-parser-js": "^1.0.41", "url-parse": "^1.4.1", "uuid": "^11.1.1", From 9a5a92aee81670535de3a748c940e7474856b6a7 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Thu, 6 Aug 2026 14:38:11 +1000 Subject: [PATCH 2/6] Loading fix for dependencies that broke during security updates --- __tests__/server/contentful.js | 65 +++++++++++++++++ .../shared/containers/ContentfulLoader.jsx | 38 ++++++++++ __tests__/shared/utils/SSR.jsx | 38 ++++++++++ docs/security-configuration.md | 15 ++++ src/server/services/contentful.js | 73 ++++++++++--------- src/shared/containers/ContentfulLoader.jsx | 17 +++-- src/shared/utils/SSR.jsx | 14 ++-- 7 files changed, 214 insertions(+), 46 deletions(-) create mode 100644 __tests__/server/contentful.js create mode 100644 __tests__/shared/containers/ContentfulLoader.jsx create mode 100644 __tests__/shared/utils/SSR.jsx diff --git a/__tests__/server/contentful.js b/__tests__/server/contentful.js new file mode 100644 index 000000000..c8a08480e --- /dev/null +++ b/__tests__/server/contentful.js @@ -0,0 +1,65 @@ +import config from 'config'; +import { createClient } from 'contentful'; + +import { getService } from 'server/services/contentful'; + +jest.mock('contentful', () => ({ + createClient: jest.fn(() => ({})), +})); +jest.mock('contentful-management', () => ({ + createClient: jest.fn(), +})); +jest.mock('topcoder-react-lib', () => ({ + logger: { log: jest.fn() }, +})); +jest.mock('topcoder-react-utils', () => ({ + isomorphy: { isDev: false }, +})); + +describe('server Contentful service configuration', () => { + const originalDefault = config.SECRET.CONTENTFUL.default; + const originalUnrelated = config.SECRET.CONTENTFUL.unrelated; + + beforeAll(() => { + config.SECRET.CONTENTFUL.default = { + SPACE_ID: 'default-space', + master: { + CDN_API_KEY: 'default-cdn-token', + PREVIEW_API_KEY: '', + }, + }; + config.SECRET.CONTENTFUL.unrelated = { + SPACE_ID: '', + master: { + CDN_API_KEY: '', + PREVIEW_API_KEY: '', + }, + }; + }); + + afterAll(() => { + config.SECRET.CONTENTFUL.default = originalDefault; + if (originalUnrelated) { + config.SECRET.CONTENTFUL.unrelated = originalUnrelated; + } else { + delete config.SECRET.CONTENTFUL.unrelated; + } + }); + + test('creates and caches only the requested delivery client', () => { + const first = getService('default', 'master', false); + const second = getService('default', 'master', false); + + expect(first).toBe(second); + expect(createClient).toHaveBeenCalledTimes(1); + expect(createClient).toHaveBeenCalledWith(expect.objectContaining({ + accessToken: 'default-cdn-token', + space: 'default-space', + })); + expect(createClient.mock.calls[0][0].host).toBeUndefined(); + + expect(() => getService('unrelated', 'master', false)) + .toThrow(/unrelated.*CONTENTFUL_UNRELATED_SPACE_ID.*CONTENTFUL_UNRELATED_CDN_API_KEY/); + expect(createClient).toHaveBeenCalledTimes(1); + }); +}); diff --git a/__tests__/shared/containers/ContentfulLoader.jsx b/__tests__/shared/containers/ContentfulLoader.jsx new file mode 100644 index 000000000..67bba1470 --- /dev/null +++ b/__tests__/shared/containers/ContentfulLoader.jsx @@ -0,0 +1,38 @@ +import { mapDispatchToProps } from 'containers/ContentfulLoader'; + +jest.mock('actions/contentful', () => { + const action = type => (...args) => ({ payload: args, type }); + return { + __esModule: true, + default: { + contentful: { + bookContent: action('BOOK_CONTENT'), + bookQuery: action('BOOK_QUERY'), + freeContent: action('FREE_CONTENT'), + freeQuery: action('FREE_QUERY'), + getContentDone: action('GET_CONTENT_DONE'), + getContentInit: action('GET_CONTENT_INIT'), + queryContentDone: action('QUERY_CONTENT_DONE'), + queryContentInit: action('QUERY_CONTENT_INIT'), + }, + }, + }; +}); +jest.mock('utils/SSR', () => () => Component => Component); + +test('returns the Redux middleware promises for asynchronous loads', () => { + const middlewarePromise = Promise.resolve('dispatched'); + const dispatch = jest.fn(() => middlewarePromise); + const mapped = mapDispatchToProps(dispatch); + + const getResult = mapped.getContent( + 'entry-id', 'entries', false, 'default', 'master', + ); + const queryResult = mapped.queryContent( + 'query-id', { content_type: 'route' }, 'entries', false, 'default', 'master', + ); + + expect(getResult).toBe(middlewarePromise); + expect(queryResult).toBe(middlewarePromise); + expect(dispatch).toHaveBeenCalledTimes(4); +}); diff --git a/__tests__/shared/utils/SSR.jsx b/__tests__/shared/utils/SSR.jsx new file mode 100644 index 000000000..844c12c96 --- /dev/null +++ b/__tests__/shared/utils/SSR.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import ReactDOMServer from 'react-dom/server'; +import { StaticRouter } from 'react-router-dom'; + +import SSR from 'utils/SSR'; + +jest.mock('topcoder-react-utils', () => ({ + isomorphy: { + isClientSide: jest.fn(() => false), + isServerSide: jest.fn(() => true), + }, + webpack: { + requireWeak: jest.fn(() => jest.requireActual('react-dom/server')), + }, +})); + +test('registers the complete store-update and rerender promise with SSR', () => { + const rerenderPromise = Promise.resolve(); + const updatePromise = { + then: jest.fn(() => rerenderPromise), + }; + const updateStore = jest.fn(() => updatePromise); + const Wrapped = SSR(() => false, updateStore)(() => null); + const staticContext = { + request: {}, + ssrPromises: [], + store: {}, + }; + + ReactDOMServer.renderToString(( + + + + )); + + expect(updatePromise.then).toHaveBeenCalledTimes(1); + expect(staticContext.ssrPromises).toEqual([rerenderPromise]); +}); diff --git a/docs/security-configuration.md b/docs/security-configuration.md index 004d2f9ac..09b48d11a 100644 --- a/docs/security-configuration.md +++ b/docs/security-configuration.md @@ -30,6 +30,21 @@ running container from the deployment platform's secret store. Rotate a credential immediately if it has ever been committed, exposed in a build log, or embedded in an image layer. +### Contentful + +Published content in the default space uses `CONTENTFUL_SPACE_ID` and +`CONTENTFUL_CDN_API_KEY`. Preview requests additionally require +`CONTENTFUL_PREVIEW_API_KEY`. Configure the corresponding space-prefixed +variables (for example, `CONTENTFUL_EDU_SPACE_ID` and +`CONTENTFUL_EDU_CDN_API_KEY`) only for the additional spaces enabled in that +deployment. + +Contentful clients are created lazily for the requested space, environment, +and delivery mode. A missing optional-space or preview credential therefore +does not disable unrelated routes. A request that actually needs an +unconfigured Contentful service reports the exact missing runtime variable; +the deployment must supply that credential for the CMS content to render. + ## Browser-visible integration keys `AUTH0_CLIENT_ID`, `FILESTACK_API_KEY`, `SEGMENT_IO_API_KEY`, and diff --git a/src/server/services/contentful.js b/src/server/services/contentful.js index 3ea968a50..b8cdc2c84 100644 --- a/src/server/services/contentful.js +++ b/src/server/services/contentful.js @@ -179,33 +179,7 @@ export function articleVote(body) { .then(entry => entry.publish()); } -let services; - -function initServiceInstances() { - const contentfulConfig = _.omit(config.SECRET.CONTENTFUL, [ - 'DEFAULT_SPACE_NAME', 'DEFAULT_ENVIRONMENT', 'MANAGEMENT_TOKEN', - ]); - services = {}; - _.map(contentfulConfig, (spaceConfig, spaceName) => { - services[spaceName] = {}; - _.map(spaceConfig, (env, name) => { - if (name !== 'SPACE_ID') { - const environment = name; - const spaceId = spaceConfig.SPACE_ID; - const previewBaseUrl = `${PREVIEW_URL}/${spaceId}/environments/${environment}`; - const cdnBaseUrl = `${CDN_URL}/${spaceId}/environments/${environment}`; - const svcs = {}; - - svcs.previewService = new ApiService( - previewBaseUrl.toString(), env.PREVIEW_API_KEY, spaceId, true, - ); - svcs.cdnService = new ApiService(cdnBaseUrl.toString(), env.CDN_API_KEY, spaceId); - services[spaceName][environment] = svcs; - } - }); - }); - return services; -} +const services = new Map(); /** * get space id for the given space name. @@ -221,21 +195,50 @@ export function getSpaceId(spaceName) { * @param {String} spaceName * @param {String} environment * @param {Boolean} preview + * @return {ApiService} Cached service for the requested Contentful API. + * @throws {Error} If the requested space, environment, or runtime credentials + * are not configured. */ export function getService(spaceName, environment, preview) { - if (!services) { - services = initServiceInstances(); - } const name = spaceName || config.CONTENTFUL.DEFAULT_SPACE_NAME; const env = environment || config.CONTENTFUL.DEFAULT_ENVIRONMENT; + const contentfulConfig = config.SECRET.CONTENTFUL; - if (!services[name]) { + if (!Object.prototype.hasOwnProperty.call(contentfulConfig, name)) { throw new Error(`space : '${name}' is not configured.`); } - if (!services[name][env]) { - throw new Error(`environment : '${env}' is not configured for space : '${name}.`); + const spaceConfig = contentfulConfig[name]; + if (!spaceConfig || !Object.prototype.hasOwnProperty.call(spaceConfig, env)) { + throw new Error(`environment : '${env}' is not configured for space : '${name}'.`); } - const service = services[name][env]; - return preview ? service.previewService : service.cdnService; + const environmentConfig = spaceConfig[env]; + const tokenName = preview ? 'PREVIEW_API_KEY' : 'CDN_API_KEY'; + const rawSpaceId = spaceConfig.SPACE_ID; + const rawAccessToken = environmentConfig && environmentConfig[tokenName]; + const spaceId = typeof rawSpaceId === 'string' ? rawSpaceId.trim() : ''; + const accessToken = typeof rawAccessToken === 'string' ? rawAccessToken.trim() : ''; + + if (!spaceId || !accessToken) { + const variablePrefix = name === 'default' + ? 'CONTENTFUL' + : `CONTENTFUL_${name.toUpperCase()}`; + const missingVariables = []; + if (!spaceId) missingVariables.push(`${variablePrefix}_SPACE_ID`); + if (!accessToken) missingVariables.push(`${variablePrefix}_${preview ? 'PREVIEW' : 'CDN'}_API_KEY`); + throw new Error( + `Contentful ${preview ? 'preview' : 'CDN'} service for space '${name}' and environment '${env}' is unavailable: missing ${missingVariables.join(', ')}.`, + ); + } + + const cacheKey = JSON.stringify([name, env, Boolean(preview)]); + if (!services.has(cacheKey)) { + const baseUrl = preview ? PREVIEW_URL : CDN_URL; + const apiBaseUrl = `${baseUrl}/${spaceId}/environments/${env}`; + services.set( + cacheKey, + new ApiService(apiBaseUrl, accessToken, spaceId, preview), + ); + } + return services.get(cacheKey); } diff --git a/src/shared/containers/ContentfulLoader.jsx b/src/shared/containers/ContentfulLoader.jsx index 858642d33..a577cbe41 100644 --- a/src/shared/containers/ContentfulLoader.jsx +++ b/src/shared/containers/ContentfulLoader.jsx @@ -437,7 +437,14 @@ function mapStateToProps(state, ownProps) { return ownProps.preview ? st.preview : st.published; } -function mapDispatchToProps(dispatch) { +/** + * Creates dispatch callbacks used by ContentfulLoader. Async callbacks return + * the Redux middleware promise so SSR can observe dispatch failures. + * @param {Function} dispatch Redux store dispatch function. + * @return {Object} ContentfulLoader dispatch callbacks. + * @throws {Error} Propagates synchronous errors raised by dispatch. + */ +export function mapDispatchToProps(dispatch) { const a = actions.contentful; const bC = a.bookContent; const bQ = a.bookQuery; @@ -452,16 +459,16 @@ function mapDispatchToProps(dispatch) { const uuid = shortId(); dispatch(a.getContentInit(uuid, contentId, target, preview, spaceName, environment)); const action = a.getContentDone(uuid, contentId, target, preview, spaceName, environment); - dispatch(action); - return action.payload; + /* Return redux-promise's chain so SSR observes dispatch failures. */ + return dispatch(action); }, queryContent: (queryId, query, target, preview, spaceName, environment) => { const uuid = shortId(); const q = _.isObject(query) ? query : null; dispatch(a.queryContentInit(uuid, queryId, target, preview, spaceName, environment)); const action = a.queryContentDone(uuid, queryId, target, q, preview, spaceName, environment); - dispatch(action); - return action.payload; + /* Return redux-promise's chain so SSR observes dispatch failures. */ + return dispatch(action); }, }; } diff --git a/src/shared/utils/SSR.jsx b/src/shared/utils/SSR.jsx index f2e7808d6..4417aaf00 100644 --- a/src/shared/utils/SSR.jsx +++ b/src/shared/utils/SSR.jsx @@ -47,7 +47,8 @@ export async function DoSSR(request, store, App) { * with the rendering of decorated component, using updated store for that. * @param {Function} updateStore Given Redux store and ExpressJS HTTP request, * as its two arguments, this function should update the store to the necessary - * state. It should return a promise that resolves when ready. + * state. It should return a promise that resolves when ready. Rejections stay + * on the promise collected by DoSSR for the renderer's normal error handling. */ export default function SSR(checkStore, updateStore) { return Component => (props) => { @@ -55,11 +56,7 @@ export default function SSR(checkStore, updateStore) { const Wrapper = withRouter(({ location, staticContext }) => { const { request, ssrPromises, store } = staticContext; if (checkStore(store, props, request)) return ; - const promise = updateStore(store, props, request); - if (ssrPromises) { - ssrPromises.push(promise); - } - promise.then(() => { + const promise = updateStore(store, props, request).then(() => { ReactDOM.renderToString(( @@ -68,6 +65,11 @@ export default function SSR(checkStore, updateStore) { )); }); + if (ssrPromises) { + ssrPromises.push(promise); + } else { + promise.catch(() => undefined); + } return null; }); return ; From 0f85b2b55963c18d6a38fabfccd76aa9ea217cf6 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Thu, 6 Aug 2026 15:35:15 +1000 Subject: [PATCH 3/6] Build fix --- Dockerfile | 8 ++++++ __tests__/config/security.js | 46 ++++++++++++++++++++++++++++++++-- build.sh | 11 +++++--- docs/security-configuration.md | 35 ++++++++++++++++---------- 4 files changed, 82 insertions(+), 18 deletions(-) diff --git a/Dockerfile b/Dockerfile index c2e2659ea..870276a7f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -110,6 +110,14 @@ COPY --from=build --chown=node:node /opt/app/config/contentful ./config/contentf COPY --chown=node:node package.json ./package.json COPY --chown=node:node bin/runtime.js ./bin/runtime.js +# Parameter Store supplies the default Contentful delivery configuration during +# deployment builds. Declare it after all filesystem layers so secret rotation +# does not invalidate them or expose the token to any RUN instruction. +ARG CONTENTFUL_CDN_API_KEY +ARG CONTENTFUL_SPACE_ID +ENV CONTENTFUL_CDN_API_KEY=${CONTENTFUL_CDN_API_KEY} \ + CONTENTFUL_SPACE_ID=${CONTENTFUL_SPACE_ID} + USER node EXPOSE 3000 diff --git a/__tests__/config/security.js b/__tests__/config/security.js index 1636b629e..56c07e95f 100644 --- a/__tests__/config/security.js +++ b/__tests__/config/security.js @@ -87,7 +87,7 @@ describe('credential configuration', () => { }, ); - test('Docker builds receive only non-secret build arguments', () => { + test('Docker builds receive only approved build arguments', () => { const source = fs.readFileSync( nodePath.resolve(__dirname, '../../build.sh'), 'utf8', @@ -95,7 +95,49 @@ describe('credential configuration', () => { const buildArguments = [...source.matchAll(/--build-arg\s+["']?([A-Z0-9_]+)/g)] .map(match => match[1]); - expect(buildArguments).toEqual(['CDN_URL', 'NODE_CONFIG_ENV']); + expect(buildArguments).toEqual([ + 'CDN_URL', + 'CONTENTFUL_CDN_API_KEY', + 'CONTENTFUL_SPACE_ID', + 'NODE_CONFIG_ENV', + ]); + expect(source).toContain( + [ + ': "$', + '{CONTENTFUL_CDN_API_KEY:?CONTENTFUL_CDN_API_KEY must be set by the build environment}"', + ].join(''), + ); + expect(source).toContain( + [ + ': "$', + '{CONTENTFUL_SPACE_ID:?CONTENTFUL_SPACE_ID must be set by the build environment}"', + ].join(''), + ); + }); + + test('runtime image preserves default Contentful delivery build arguments', () => { + const source = fs.readFileSync( + nodePath.resolve(__dirname, '../../Dockerfile'), + 'utf8', + ); + const runtimeMarker = ['FROM $', '{NODE_IMAGE} AS runtime'].join(''); + const runtimeStart = source.indexOf(runtimeMarker); + const buildStage = source.slice(0, runtimeStart); + const runtimeStage = source.slice(runtimeStart); + const contentfulArgumentsStart = runtimeStage.indexOf('ARG CONTENTFUL_CDN_API_KEY'); + + expect(runtimeStart).toBeGreaterThan(-1); + expect(buildStage).not.toContain('CONTENTFUL_CDN_API_KEY'); + expect(buildStage).not.toContain('CONTENTFUL_SPACE_ID'); + expect(contentfulArgumentsStart).toBeGreaterThan(runtimeStage.lastIndexOf('COPY ')); + expect(runtimeStage).toContain('ARG CONTENTFUL_CDN_API_KEY'); + expect(runtimeStage).toContain('ARG CONTENTFUL_SPACE_ID'); + expect(runtimeStage).toContain( + ['CONTENTFUL_CDN_API_KEY=$', '{CONTENTFUL_CDN_API_KEY}'].join(''), + ); + expect(runtimeStage).toContain( + ['CONTENTFUL_SPACE_ID=$', '{CONTENTFUL_SPACE_ID}'].join(''), + ); }); test('JMeter loads M2M credentials from runtime properties', () => { diff --git a/build.sh b/build.sh index ddae84639..6b5bee3e1 100755 --- a/build.sh +++ b/build.sh @@ -1,15 +1,20 @@ #!/bin/bash set -eo pipefail -# Builds the Community App image using BuildKit's layer cache. Only public -# browser configuration is accepted as a build argument; secrets belong in the -# runtime environment. +# Builds the Community App image using BuildKit's layer cache. The deployment +# pipeline supplies the default Contentful delivery configuration from +# Parameter Store and persists it in the runtime image. TAG="community-app:latest" NODE_CONFIG_ENV="${NODE_CONFIG_ENV:-production}" +: "${CONTENTFUL_SPACE_ID:?CONTENTFUL_SPACE_ID must be set by the build environment}" +: "${CONTENTFUL_CDN_API_KEY:?CONTENTFUL_CDN_API_KEY must be set by the build environment}" + echo "NODE_CONFIG_ENV ${NODE_CONFIG_ENV}" DOCKER_BUILDKIT=1 docker build --tag "${TAG}" \ --build-arg "CDN_URL=${CDN_URL:-}" \ + --build-arg "CONTENTFUL_CDN_API_KEY=${CONTENTFUL_CDN_API_KEY}" \ + --build-arg "CONTENTFUL_SPACE_ID=${CONTENTFUL_SPACE_ID}" \ --build-arg "NODE_CONFIG_ENV=${NODE_CONFIG_ENV}" \ . diff --git a/docs/security-configuration.md b/docs/security-configuration.md index 09b48d11a..a75a8f4dc 100644 --- a/docs/security-configuration.md +++ b/docs/security-configuration.md @@ -1,9 +1,9 @@ # Security-sensitive configuration Credential defaults in the tracked configuration files are intentionally -empty. Supply credentials at runtime using the mappings in -`config/custom-environment-variables.js`; never add a real value to a tracked -configuration file. +empty. Supply credentials using the mappings in +`config/custom-environment-variables.js` and the deployment rules below; never +add a real value to a tracked configuration file. ## Server-only credentials @@ -15,7 +15,7 @@ corresponding integration is enabled: - `AUTH_SECRET` - `CHAMELEON_VERIFICATION_SECRET` - `CONTENTFUL_MANAGEMENT_TOKEN` -- the `CONTENTFUL_*_CDN_API_KEY` and `CONTENTFUL_*_PREVIEW_API_KEY` variables +- Contentful preview and non-default-space delivery credentials - `TC_M2M_CLIENT_SECRET` (along with the related M2M client configuration) - `COGNITIVE_NEWSLETTER_SIGNUP_APIKEY` - `MAILCHIMP_API_KEY` @@ -25,19 +25,28 @@ corresponding integration is enabled: - `GSHEETS_API_KEY` - `GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY` -Do not pass these values as Docker build arguments. Inject them into the -running container from the deployment platform's secret store. Rotate a -credential immediately if it has ever been committed, exposed in a build log, -or embedded in an image layer. +Except for the default Contentful delivery configuration documented below, do +not pass these values as Docker build arguments. Inject them into the running +container from the deployment platform's secret store. Rotate a credential +immediately if it has ever been committed or exposed in a build log. ### Contentful Published content in the default space uses `CONTENTFUL_SPACE_ID` and -`CONTENTFUL_CDN_API_KEY`. Preview requests additionally require -`CONTENTFUL_PREVIEW_API_KEY`. Configure the corresponding space-prefixed -variables (for example, `CONTENTFUL_EDU_SPACE_ID` and -`CONTENTFUL_EDU_CDN_API_KEY`) only for the additional spaces enabled in that -deployment. +`CONTENTFUL_CDN_API_KEY`. The current deployment pipeline loads these two +values from Parameter Store into the build environment. `build.sh` requires +them and passes them to the final Docker stage, which persists them as runtime +environment variables for `node-config`. + +This build-time exception embeds both values in the image configuration, where +users with image or registry access can recover them. Keep registry access +restricted and do not extend this mechanism to preview or management tokens. +Token rotation requires rebuilding and redeploying the image; retained older +image versions continue to contain the previous token. +Preview requests require `CONTENTFUL_PREVIEW_API_KEY`; additional spaces use +their corresponding space-prefixed variables (for example, +`CONTENTFUL_EDU_SPACE_ID` and `CONTENTFUL_EDU_CDN_API_KEY`). Inject those at +runtime only for the integrations enabled in that deployment. Contentful clients are created lazily for the requested space, environment, and delivery mode. A missing optional-space or preview credential therefore From 5a8de95366c3b58611a6e6f891e9cf9ff398f971 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Thu, 6 Aug 2026 15:57:06 +1000 Subject: [PATCH 4/6] Fix for circular initialization error --- src/shared/components/Contentful/Modal/index.jsx | 2 +- src/shared/utils/secureRandom.js | 2 +- vendor/tc-auth-lib-compat/src/connector-wrapper.js | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/shared/components/Contentful/Modal/index.jsx b/src/shared/components/Contentful/Modal/index.jsx index 55fcd996c..641870b1e 100644 --- a/src/shared/components/Contentful/Modal/index.jsx +++ b/src/shared/components/Contentful/Modal/index.jsx @@ -8,7 +8,7 @@ import ContentfulLoader from 'containers/ContentfulLoader'; import LoadingIndicator from 'components/LoadingIndicator'; import Banner from 'components/Contentful/Banner'; import ContentBlock from 'components/Contentful/ContentBlock'; -import Viewport from 'components/Contentful/Viewport'; +import { ViewportLoader as Viewport } from 'components/Contentful/Viewport'; import { Modal, PrimaryButton } from 'topcoder-react-ui-kit'; import { errors } from 'topcoder-react-lib'; import { themr } from 'react-css-super-themr'; diff --git a/src/shared/utils/secureRandom.js b/src/shared/utils/secureRandom.js index 190621fa6..63a238f5a 100644 --- a/src/shared/utils/secureRandom.js +++ b/src/shared/utils/secureRandom.js @@ -7,7 +7,7 @@ const getCryptoLibrary = () => { return nodeCrypto; }; -export default function (min, max) { +export default function getSecureRandomIndex(min, max) { const crypto = getCryptoLibrary(); const random = new Uint32Array(1); if (typeof crypto.getRandomValues === 'function') { diff --git a/vendor/tc-auth-lib-compat/src/connector-wrapper.js b/vendor/tc-auth-lib-compat/src/connector-wrapper.js index fb1e2a002..90e89b3ad 100644 --- a/vendor/tc-auth-lib-compat/src/connector-wrapper.js +++ b/vendor/tc-auth-lib-compat/src/connector-wrapper.js @@ -28,6 +28,7 @@ function configureConnector({ } if (iframe) { + // eslint-disable-next-line no-console console.warn( 'tc-accounts connector can only be configured once; this request was ignored.', ); From 2b97cc0ffdbd431b66497b09fbd19854fb30a997 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Thu, 6 Aug 2026 17:40:35 +1000 Subject: [PATCH 5/6] CSS fixes --- __tests__/config/css-modules.js | 27 +++++++++++++++++++ config/css-modules.js | 20 ++++++++------ src/server/index.js | 9 ------- .../containers/TopcoderHeader/index.jsx | 1 + src/shared/routes/index.jsx | 2 +- src/shared/routes/styles.scss | 17 ++++++++++++ 6 files changed, 58 insertions(+), 18 deletions(-) create mode 100644 __tests__/config/css-modules.js create mode 100644 src/shared/routes/styles.scss diff --git a/__tests__/config/css-modules.js b/__tests__/config/css-modules.js new file mode 100644 index 000000000..e87e6410c --- /dev/null +++ b/__tests__/config/css-modules.js @@ -0,0 +1,27 @@ +/* eslint-env jest */ + +const path = require('path'); +const { generateScopedName } = require('../../config/css-modules'); + +test('production CSS module names are valid and match css-loader identifiers', () => { + const originalBabelEnv = process.env.BABEL_ENV; + process.env.BABEL_ENV = 'production'; + + try { + const topcoderStyles = path.resolve( + __dirname, + '../../src/shared/routes/Topcoder/styles.scss', + ); + const listingStyles = path.resolve( + __dirname, + '../../src/shared/containers/challenge-listing/Listing/styles.scss', + ); + + expect(generateScopedName('container', topcoderStyles)).toBe('_10hiPS'); + expect(generateScopedName('container', listingStyles)).toBe('_3pMa6m'); + expect(generateScopedName('bannerContent', listingStyles)).toBe('JwBbj_'); + } finally { + if (originalBabelEnv === undefined) delete process.env.BABEL_ENV; + else process.env.BABEL_ENV = originalBabelEnv; + } +}); diff --git a/config/css-modules.js b/config/css-modules.js index 87cca716e..752f32f5b 100644 --- a/config/css-modules.js +++ b/config/css-modules.js @@ -54,15 +54,19 @@ function generateHash(localName, filename) { */ function generateScopedName(localName, filename) { const hash = generateHash(localName, filename); - if (process.env.BABEL_ENV === 'production') return hash.slice(0, 6); + let scopedName = hash.slice(0, 6); - const relativeFilename = path.relative(context, filename).replace(/\\/g, '/'); - const extension = path.extname(relativeFilename); - const name = path.basename(relativeFilename, extension); - const directory = path.dirname(relativeFilename) === '.' - ? '' - : `${path.dirname(relativeFilename)}/`; - return `${directory}${name}___${localName}___${hash.slice(0, 6)}` + if (process.env.BABEL_ENV !== 'production') { + const relativeFilename = path.relative(context, filename).replace(/\\/g, '/'); + const extension = path.extname(relativeFilename); + const name = path.basename(relativeFilename, extension); + const directory = path.dirname(relativeFilename) === '.' + ? '' + : `${path.dirname(relativeFilename)}/`; + scopedName = `${directory}${name}___${localName}___${scopedName}`; + } + + return scopedName .replace(new RegExp('[^a-zA-Z0-9\\-_\u00A0-\uFFFF]', 'g'), '-') .replace(/^((-?[0-9])|--)/, '_$1'); } diff --git a/src/server/index.js b/src/server/index.js index d354f5260..9c992b732 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -78,7 +78,6 @@ const sw = `sw.js${process.env.NODE_ENV === 'production' ? '' : '?debug'}`; const swScope = '/challenges'; // we are currently only interested in improving challenges pages const tcoPattern = new RegExp(/^tco\d{2}\.topcoder(?:-dev)?\.com$/i); -const universalNavUrl = config.UNIVERSAL_NAV_URL; const getExtraScripts = ts => [ ``, - ` - - `, ]; const MODE = process.env.BABEL_ENV; diff --git a/src/shared/containers/TopcoderHeader/index.jsx b/src/shared/containers/TopcoderHeader/index.jsx index ce478cb4f..9588c70f2 100644 --- a/src/shared/containers/TopcoderHeader/index.jsx +++ b/src/shared/containers/TopcoderHeader/index.jsx @@ -60,6 +60,7 @@ const TopcoderHeader = ({ auth, location }) => { return (
*:nth-child(2):not(:last-child) { + display: flex; + flex: 1 0 auto; + position: relative; + z-index: 1; + } +} From a55a9bee0a6a4491c8fde7c8c249a71adb01798c Mon Sep 17 00:00:00 2001 From: jmgasper Date: Thu, 6 Aug 2026 17:50:01 +1000 Subject: [PATCH 6/6] Revert PR #7249 and follow-up changes --- .babelrc | 2 +- .circleci/config.yml | 17 +- .dockerignore | 43 +- .eslintignore | 3 +- .eslintrc | 69 +- .github/workflows/commitlint.yml | 6 +- .github/workflows/trivy.yaml | 5 +- .npmrc | 2 - .nvmrc | 2 +- .stylelintrc | 33 +- Dockerfile | 277 +- __tests__/.eslintrc | 13 +- __tests__/config/css-modules.js | 27 - __tests__/config/security.js | 180 - __tests__/config/uninav.js | 35 - __tests__/config/webpack.js | 37 - __tests__/server/avatar.js | 101 - __tests__/server/contentful.js | 65 - __tests__/server/recruitCRM.js | 96 - __tests__/server/routes/authentication.js | 63 - __tests__/server/routes/security.js | 98 - .../{fixtures => __mocks__}/design.json | 0 .../{fixtures => __mocks__}/develop.json | 0 .../{fixtures => __mocks__}/marathon.json | 0 .../__snapshots__/index.jsx.snap | 2 +- .../shared/components/ChallengeTile/index.jsx | 6 +- .../__snapshots__/ArticleCard.jsx.snap | 2 +- .../__snapshots__/MemberCard.jsx.snap | 2 +- .../Contentful/SearchBar/SearchBar.jsx | 32 - .../Shape/__snapshots__/Shape.jsx.snap | 2 +- .../__snapshots__/ChildList.jsx.snap | 2 +- .../__snapshots__/ChildListRow.jsx.snap | 2 +- .../__snapshots__/TracksTree.jsx.snap | 2 +- .../Checkbox/__snapshots__/index.jsx.snap | 2 +- .../Datepicker/__snapshots__/index.jsx.snap | 2 +- .../Dropdown/__snapshots__/index.jsx.snap | 2 +- .../__snapshots__/index.jsx.snap | 2 +- .../RadioButton/__snapshots__/index.jsx.snap | 2 +- .../TextInput/__snapshots__/index.jsx.snap | 2 +- .../Textarea/__snapshots__/index.jsx.snap | 2 +- .../Toggles/__snapshots__/index.jsx.snap | 2 +- .../Header/__snapshots__/index.jsx.snap | 2 +- .../__snapshots__/LeaderboardTable.jsx.snap | 10 +- .../Leaderboard/__snapshots__/Podium.jsx.snap | 2 +- .../__snapshots__/PodiumSpot.jsx.snap | 10 +- .../Loader/__snapshots__/Loader.jsx.snap | 2 +- .../__snapshots__/LoaderExamples.jsx.snap | 2 +- .../Popup/__snapshots__/index.jsx.snap | 2 +- .../BadgesModal/__snapshots__/index.jsx.snap | 3 +- .../ExternalLink/__snapshots__/index.jsx.snap | 2 +- .../GalleryModal/__snapshots__/index.jsx.snap | 2 +- .../{fixtures => __mocks__}/develop.json | 0 .../__snapshots__/index.jsx.snap | 2 +- .../Stats/SubTrackChallengeView/index.jsx | 2 +- .../ProfilePage/__snapshots__/index.jsx.snap | 2 +- .../SRMTile/__snapshots__/index.jsx.snap | 2 +- .../__snapshots__/ScreeningDetails.jsx.snap | 6 +- .../__snapshots__/ScreeningStatus.jsx.snap | 6 +- .../__snapshots__/Submission.jsx.snap | 2 +- .../__snapshots__/SubmissionsTable.jsx.snap | 2 +- .../FilestackFilePicker/index.jsx | 101 +- .../SubmissionPage/Submit/index.jsx | 2 - .../SubMenu/__snapshots__/Item.jsx.snap | 2 +- .../TrackIcon/__snapshots__/index.jsx.snap | 2 +- .../components/__snapshots__/Button.jsx.snap | 2 +- .../components/__snapshots__/Content.jsx.snap | 2 +- .../components/__snapshots__/Handle.jsx.snap | 2 +- .../__snapshots__/LoadingIndicator.jsx.snap | 2 +- .../components/__snapshots__/Select.jsx.snap | 2 +- .../__snapshots__/SortingSelectBar.jsx.snap | 2 +- .../components/__snapshots__/Switch.jsx.snap | 2 +- .../__snapshots__/SwitchWithLabel.jsx.snap | 2 +- .../__snapshots__/TopcoderFooter.jsx.snap | 2 +- .../Header/DeadlinesPanel.jsx | 20 - .../Header/__snapshots__/Prizes.jsx.snap | 2 +- .../Winner/__snapshots__/index.jsx.snap | 2 +- .../__snapshots__/Status.jsx.snap | 2 +- .../__snapshots__/index.jsx.snap | 2 +- .../__snapshots__/ChallengeSearchBar.jsx.snap | 2 +- .../__snapshots__/DateRangePicker.jsx.snap | 2 +- .../__snapshots__/FiltersCardsType.jsx.snap | 2 +- .../__snapshots__/FiltersPanel.jsx.snap | 2 +- .../__snapshots__/ArrowsMoveVertical.jsx.snap | 2 +- .../Listing/__snapshots__/Bucket.jsx.snap | 2 +- .../SRMCard/__snapshots__/index.jsx.snap | 2 +- .../__snapshots__/ProgressBarTooltip.jsx.snap | 2 +- .../TrackAbbreviationTooltip.jsx.snap | 2 +- .../__snapshots__/UserAvatarTooltip.jsx.snap | 2 +- .../__snapshots__/LeaderboardAvatar.jsx.snap | 2 +- .../__snapshots__/index.jsx.snap | 2 +- .../__snapshots__/ChallengeCard.jsx.snap | 2 +- .../Themr/__snapshots__/index.jsx.snap | 2 +- .../__snapshots__/CssModules.jsx.snap | 2 +- .../examples/__snapshots__/DataFetch.jsx.snap | 2 +- .../examples/__snapshots__/FontsTest.jsx.snap | 2 +- .../__snapshots__/SvgLoading.jsx.snap | 37 +- .../__snapshots__/AccessDenied.jsx.snap | 2 +- .../__snapshots__/Accordion.jsx.snap | 2 +- .../__snapshots__/AccordionItem.jsx.snap | 2 +- .../__snapshots__/Banner.jsx.snap | 2 +- .../__snapshots__/CommunityStats.jsx.snap | 2 +- .../__snapshots__/Dropdown.jsx.snap | 2 +- .../__snapshots__/Header.jsx.snap | 2 +- .../__snapshots__/IconStat.jsx.snap | 2 +- .../__snapshots__/ImageText.jsx.snap | 2 +- .../__snapshots__/JoinCommunity.jsx.snap | 6 +- .../__snapshots__/NewsSection.jsx.snap | 2 +- .../community-2/__snapshots__/Home.jsx.snap | 2 +- .../community-2/__snapshots__/Learn.jsx.snap | 4 +- .../demo-expert/__snapshots__/Learn.jsx.snap | 2 +- .../taskforce/__snapshots__/Home.jsx.snap | 2 +- .../tc-prod-dev/__snapshots__/Learn.jsx.snap | 2 +- .../wipro/__snapshots__/Learn.jsx.snap | 2 +- .../shared/containers/ContentfulLoader.jsx | 38 - .../__snapshots__/DevTools.jsx.snap | 2 +- .../__snapshots__/Leaderboard.jsx.snap | 2 +- .../__snapshots__/TopcoderHeader.jsx.snap | 2 +- .../examples/__snapshots__/DataFetch.jsx.snap | 2 +- .../shared/reducers/examples/data-fetch.js | 28 +- .../tc-communities/__snapshots__/meta.js.snap | 116 +- .../shared/reducers/tc-communities/meta.js | 31 +- .../Examples/__snapshots__/DataFetch.jsx.snap | 2 +- .../Examples/__snapshots__/index.jsx.snap | 2 +- .../routes/__snapshots__/index.jsx.snap | 2 +- __tests__/shared/services/money.js | 37 - __tests__/shared/utils/SSR.jsx | 38 - .../utils/__snapshots__/markdown.js.snap | 28 +- automated-smoke-test/README.md | 12 +- automated-smoke-test/package-lock.json | 4712 +- automated-smoke-test/package.json | 61 +- .../pages/topcoder/login/login.po.ts | 7 +- .../profile/basic-info/basic-info.po.ts | 2 +- automated-smoke-test/test-data/test-data.json | 3 +- automated-smoke-test/utils/config-helper.ts | 7 +- bin/runtime.js | 11 - bin/www | 3 +- build.sh | 89 +- config/babel/create.js | 89 - config/babel/node.js | 36 - config/babel/webpack.js | 14 - config/backup-default.js | 24 +- config/css-modules.js | 99 - config/default.js | 24 +- config/development.js | 4 +- config/jest/default.js | 50 +- config/jest/setup.js | 12 +- config/production.js | 2 +- config/qa.js | 4 +- config/test.js | 2 +- config/webpack/browser/config.js | 6 - config/webpack/browser/tc-core-library-js.js | 20 - config/webpack/create.js | 330 - config/webpack/default.js | 103 + config/webpack/development.js | 31 +- config/webpack/production.js | 41 +- config/webpack/qa.js | 46 +- docs/security-configuration.md | 74 - package-lock.json | 44837 +++++++++------- package.json | 272 +- pom.xml | 79 +- src/server/index.js | 47 +- src/server/routes/authentication.js | 87 - src/server/routes/contentful.js | 37 +- src/server/routes/recruitCRM.js | 38 +- src/server/services/avatar.js | 151 +- src/server/services/contentful.js | 73 +- src/server/services/recruitCRM.js | 245 +- src/server/sw.js | 279 +- src/shared/actions/gSheet.js | 1 + .../ContentSlider/ContentSlider.jsx | 1 + .../components/Contentful/Modal/index.jsx | 2 +- .../Contentful/SearchBar/SearchBar.jsx | 45 +- .../components/GUIKit/JobListCard/index.jsx | 2 +- .../HallOfFamePage/FunFacts/index.jsx | 19 +- src/shared/components/InputSelect/index.jsx | 4 +- src/shared/components/Notifications/index.jsx | 6 +- .../Activity/ActivityCard/index.jsx | 6 +- .../Activity/ActivityCard/styles.scss | 2 - .../ProfilePage/BadgesModal/achievementMap.js | 2 +- .../Stats/DistributionGraph/index.jsx | 2 +- .../ProfilePage/Stats/HistoryGraph/index.jsx | 2 +- .../components/SecurityReminder/index.jsx | 2 + .../FilestackFilePicker/index.jsx | 39 +- .../HowToCompetePage/Header/index.jsx | 22 +- .../HowToCompetePage/QAComponent/index.jsx | 14 +- .../HowToCompetePage/StepByStep/index.jsx | 14 +- .../challenge-detail/Checkpoints/index.jsx | 1 + .../Header/DeadlinesPanel/index.jsx | 1 + .../Specification/SideBar/ShareSocial.jsx | 2 + .../Filters/FiltersPanel/index.jsx | 2 + .../Listing/Bucket/index.jsx | 2 + .../placeholders/ChallengeCard/index.jsx | 4 + .../components/examples/BlogFeed/index.jsx | 2 +- .../examples/ChallengesFeed/index.jsx | 2 +- .../examples/CodeSplitting/index.jsx | 8 +- .../components/examples/GigsFeed/index.jsx | 2 +- .../examples/ThriveArticlesFeed/index.jsx | 2 +- .../communities/cognitive/Resources/index.jsx | 18 +- .../communities/community-2/Learn/index.jsx | 2 +- .../iot/AssetDetail/ShareSocial.jsx | 2 + src/shared/containers/ContentfulLoader.jsx | 17 +- .../containers/Gigs/RecruitCRMJobApply.jsx | 2 +- src/shared/containers/Gigs/RecruitCRMJobs.jsx | 2 +- .../containers/Gigs/_RecruitCRMJobs_ab-v1.jsx | 2 +- .../containers/TopcoderHeader/index.jsx | 1 - .../reducers/challenge-listing/index.js | 42 +- .../reducers/challenge-listing/sidebar.js | 1 + src/shared/routes/TimelineWall/Router.jsx | 2 + src/shared/routes/index.jsx | 2 +- src/shared/routes/styles.scss | 17 - src/shared/services/money.js | 48 +- src/shared/utils/SSR.jsx | 14 +- src/shared/utils/secureRandom.js | 2 +- src/shared/utils/withOptimizely.jsx | 23 - src/shared/utils/xml2json.js | 30 +- src/test/jmeter/Community-25UV.jmx | 4 +- vendor/glob-compat/README.md | 13 - vendor/glob-compat/browser.js | 37 - vendor/glob-compat/index.js | 43 - vendor/glob-compat/package.json | 14 - vendor/minimatch-compat/README.md | 9 - vendor/minimatch-compat/index.js | 21 - vendor/minimatch-compat/package.json | 13 - vendor/tc-auth-lib-compat/README.md | 11 - vendor/tc-auth-lib-compat/index.js | 15 - vendor/tc-auth-lib-compat/package.json | 13 - .../src/connector-wrapper.js | 130 - vendor/tc-auth-lib-compat/src/iframe.js | 24 - vendor/tc-auth-lib-compat/src/token.js | 121 - webpack.config.js | 14 +- 230 files changed, 27377 insertions(+), 27505 deletions(-) delete mode 100644 .npmrc delete mode 100644 __tests__/config/css-modules.js delete mode 100644 __tests__/config/security.js delete mode 100644 __tests__/config/uninav.js delete mode 100644 __tests__/config/webpack.js delete mode 100644 __tests__/server/avatar.js delete mode 100644 __tests__/server/contentful.js delete mode 100644 __tests__/server/recruitCRM.js delete mode 100644 __tests__/server/routes/authentication.js delete mode 100644 __tests__/server/routes/security.js rename __tests__/shared/components/ChallengeTile/{fixtures => __mocks__}/design.json (100%) rename __tests__/shared/components/ChallengeTile/{fixtures => __mocks__}/develop.json (100%) rename __tests__/shared/components/ChallengeTile/{fixtures => __mocks__}/marathon.json (100%) delete mode 100644 __tests__/shared/components/Contentful/SearchBar/SearchBar.jsx rename __tests__/shared/components/ProfilePage/Stats/SubTrackChallengeView/{fixtures => __mocks__}/develop.json (100%) delete mode 100644 __tests__/shared/components/challenge-detail/Header/DeadlinesPanel.jsx delete mode 100644 __tests__/shared/containers/ContentfulLoader.jsx delete mode 100644 __tests__/shared/services/money.js delete mode 100644 __tests__/shared/utils/SSR.jsx delete mode 100644 bin/runtime.js delete mode 100644 config/babel/create.js delete mode 100644 config/babel/node.js delete mode 100644 config/babel/webpack.js delete mode 100644 config/css-modules.js delete mode 100644 config/webpack/browser/config.js delete mode 100644 config/webpack/browser/tc-core-library-js.js delete mode 100644 config/webpack/create.js create mode 100644 config/webpack/default.js delete mode 100644 docs/security-configuration.md delete mode 100644 src/server/routes/authentication.js delete mode 100644 src/shared/routes/styles.scss delete mode 100644 src/shared/utils/withOptimizely.jsx delete mode 100644 vendor/glob-compat/README.md delete mode 100644 vendor/glob-compat/browser.js delete mode 100644 vendor/glob-compat/index.js delete mode 100644 vendor/glob-compat/package.json delete mode 100644 vendor/minimatch-compat/README.md delete mode 100644 vendor/minimatch-compat/index.js delete mode 100644 vendor/minimatch-compat/package.json delete mode 100644 vendor/tc-auth-lib-compat/README.md delete mode 100644 vendor/tc-auth-lib-compat/index.js delete mode 100644 vendor/tc-auth-lib-compat/package.json delete mode 100644 vendor/tc-auth-lib-compat/src/connector-wrapper.js delete mode 100644 vendor/tc-auth-lib-compat/src/iframe.js delete mode 100644 vendor/tc-auth-lib-compat/src/token.js diff --git a/.babelrc b/.babelrc index 26ac1c7b4..68ba92054 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,6 @@ { "presets": [ - ["./config/babel/node", { + ["topcoder-react-utils/config/babel/node-ssr", { "baseAssetsOutputPath": "/community-app-assets" }] ] diff --git a/.circleci/config.yml b/.circleci/config.yml index df5535e2e..40e916d30 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -111,11 +111,11 @@ jobs: # Jest unit-tests). test: docker: - - image: cimg/node:24.18.0@sha256:4a638ad75f8601fec1f193e71df584639e6accb1771cea23f3d9d3857bca480c + - image: circleci/node:10.24.1 steps: - checkout - restore_cache: - key: test-node24-modules-{{ checksum "package-lock.json" }} + key: test-node-modules-{{ checksum "package-lock.json" }} - run: name: Config Git command: git config --global url."https://git@".insteadOf git:// @@ -124,14 +124,15 @@ jobs: command: npm ci no_output_timeout: 20m - save_cache: - key: test-node24-modules-{{ checksum "package-lock.json" }} + key: test-node-modules-{{ checksum "package-lock.json" }} paths: - node_modules - - run: npm run lint && npm run jest:ci + - run: npm test Performance-Testing: docker: - - image: cimg/openjdk:17.0.19@sha256:09490c4b3e6e85f8b382c7ce0ef70aa8e940ee3a3567a11d2aede0ed094dc525 + # specify the version you desire here + - image: circleci/openjdk:8-jdk # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images @@ -150,16 +151,16 @@ jobs: # Download and cache dependencies - restore_cache: keys: - - performance-jdk17-maven3-{{ checksum "pom.xml" }} + - v1-dependencies-{{ checksum "pom.xml" }} # fallback to using the latest cache if no exact match is found - - performance-jdk17-maven3- + - v1-dependencies- - run: mvn dependency:go-offline - save_cache: paths: - ~/.m2 - key: performance-jdk17-maven3-{{ checksum "pom.xml" }} + key: v1-dependencies-{{ checksum "pom.xml" }} - run: mvn verify diff --git a/.dockerignore b/.dockerignore index ac650ed4c..1dba739ce 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,40 +1,3 @@ -# Version-control and local editor state -.git -.github -.circleci -.idea -.vscode -*.swp -*.swo - -# Local dependencies, generated output, and test artifacts -node_modules -automated-smoke-test -build -.build-info -target -coverage -__coverage__ -.nyc_output -*.log -npm-debug.log* - -# Local environment and deployment material must never enter the build context -.env -.env.* -!.env.example -*.key -*.pem -awsenvconf -buildvar_env -deployvar_env - -# Files that are not needed to install, test, or build the application -docs -README.md -CHANGELOG.md -CONTRIBUTING.md -LICENSE -CODEOWNERS -pom.xml -*.patch +__coverage__/ +.git/ +node_modules/ \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index 5408f9f51..42dd1b146 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,3 @@ __coverage__ -automated-smoke-test/temp build -node_modules +node_modules \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index c9fde00e2..c323322f5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,69 +1,12 @@ { - "extends": "airbnb", - "parser": "@babel/eslint-parser", - "parserOptions": { - "requireConfigFile": false, - "sourceType": "module", - "babelOptions": { - "babelrc": false, - "configFile": false, - "presets": ["@babel/preset-react"], - "plugins": [ - "@babel/plugin-proposal-export-default-from", - "@babel/plugin-transform-class-properties" - ] - } - }, + "extends": "./node_modules/topcoder-react-utils/config/eslint/default.json", "rules": { - "arrow-parens": ["error", "as-needed", { "requireForBlockBody": true }], - "class-methods-use-this": "off", - "default-param-last": "off", - "function-call-argument-newline": "off", - "function-paren-newline": ["error", "consistent"], - "import/no-import-module-exports": "off", - "jsx-a11y/anchor-is-valid": "off", - "jsx-a11y/control-has-associated-label": "off", - "jsx-a11y/href-no-hash": "off", - "max-classes-per-file": "off", + "jsx-a11y/anchor-is-valid": false, "import/no-cycle": [2, { "maxDepth": 1 }], - "no-multiple-empty-lines": ["error", { "max": 2, "maxBOF": 2, "maxEOF": 0 }], - "no-promise-executor-return": "off", - "no-redeclare": ["error", { "builtinGlobals": false }], - "prefer-regex-literals": "off", - "react/forbid-prop-types": "off", - "react/function-component-definition": "off", - "react/jsx-curly-brace-presence": "off", - "react/jsx-curly-newline": "off", - "react/jsx-fragments": "off", - "react/jsx-no-useless-fragment": "off", - "react/jsx-one-expression-per-line": "off", - "react/jsx-props-no-spreading": "off", - "react/no-deprecated": "off", - "react/no-invalid-html-attribute": "off", - "react/no-unstable-nested-components": "off", - "react/no-unused-class-component-methods": "off", - "react/no-unknown-property": ["error", { "ignore": ["styleName"] }], - "react/sort-comp": "off" + "react/forbid-prop-types": false, + "react/no-unknown-property": ["error", { "ignore": ["styleName"] }] }, "env": { - "browser": true, - "es6": true, - "node": true - }, - "settings": { - "import/resolver": { - "node": { - "extensions": [".js", ".jsx"], - "moduleDirectory": ["node_modules", "src/shared", "src"] - } - } - }, - "overrides": [ - { - "files": ["config/**/*.js", "webpack.config.js"], - "rules": { - "import/no-extraneous-dependencies": ["error", { "devDependencies": true }] - } - } - ] + "browser": true + } } diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index f2eeb3db8..795006101 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -1,10 +1,6 @@ name: Commitlint on: [pull_request] -permissions: - contents: read - pull-requests: read - jobs: commit-lint: runs-on: ubuntu-latest @@ -16,4 +12,4 @@ jobs: fetch-depth: 0 - uses: wagoid/commitlint-github-action@v1.4.0 with: - configFile: './.commitlintrc.yml' + configFile: './.commitlintrc.yml' \ No newline at end of file diff --git a/.github/workflows/trivy.yaml b/.github/workflows/trivy.yaml index 97d227d3e..9cbcf5209 100644 --- a/.github/workflows/trivy.yaml +++ b/.github/workflows/trivy.yaml @@ -24,9 +24,8 @@ jobs: ignore-unfixed: true format: "sarif" output: "trivy-results.sarif" - severity: "CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN" - limit-severities-for-sarif: true - scanners: vuln,secret,misconfig + severity: "CRITICAL,HIGH,UNKNOWN" + scanners: vuln,secret,misconfig,license github-pat: ${{ secrets.GITHUB_TOKEN }} - name: Upload Trivy scan results to GitHub Security tab diff --git a/.npmrc b/.npmrc deleted file mode 100644 index a48ecb008..000000000 --- a/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -legacy-peer-deps=true -strict-allow-scripts=true diff --git a/.nvmrc b/.nvmrc index 5bcf9c6e6..c8b7cbff7 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v24.18.0 +v10.24.1 diff --git a/.stylelintrc b/.stylelintrc index ad21f237e..d2541eb3f 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -1,42 +1,11 @@ { "extends": "stylelint-config-standard", "rules": { - "alpha-value-notation": null, - "annotation-no-unknown": null, - "at-rule-descriptor-value-no-unknown": null, - "at-rule-no-vendor-prefix": null, "at-rule-no-unknown": [true, { "ignoreAtRules": ["content", "extend", "for", "include", "mixin"] }], - "color-function-alias-notation": null, - "color-function-notation": null, - "declaration-block-no-duplicate-properties": [true, { - "ignore": ["consecutive-duplicates-with-different-values"] - }], - "declaration-block-no-redundant-longhand-properties": null, - "declaration-property-value-keyword-no-deprecated": null, - "declaration-property-value-no-unknown": null, - "font-family-name-quotes": null, - "function-url-quotes": null, - "import-notation": null, - "keyframes-name-pattern": null, - "media-feature-range-notation": null, - "media-query-no-invalid": null, - "nesting-selector-no-missing-scoping-root": null, - "no-descending-specificity": null, - "no-invalid-position-at-import-rule": null, - "number-max-precision": null, - "property-no-deprecated": null, - "property-no-vendor-prefix": null, - "selector-attribute-quotes": null, - "selector-class-pattern": null, - "selector-no-vendor-prefix": null, - "selector-not-notation": null, "selector-pseudo-class-no-unknown": [true, { "ignorePseudoClasses": ["global"] - }], - "shorthand-property-no-redundant-values": null, - "value-keyword-case": null, - "value-no-vendor-prefix": null + }] } } diff --git a/Dockerfile b/Dockerfile index 870276a7f..de3ab09f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,130 +1,173 @@ -# syntax=docker/dockerfile:1.7 +# Builds production version of Community App inside Docker container, +# and runs it against the specified Topcoder backend (development or +# production) when container is executed. -# Pin the complete multi-platform image digest so builds cannot silently pick up -# a different base image. Renovate/Dependabot can update the tag and digest -# together when a patched Node image is published. -ARG NODE_IMAGE=node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436 -ARG NODE_BUILD_IMAGE=node:24.18.0-alpine3.23@sha256:595398b0081eacda8e1c4c5b97b76cd1020e4d58a8ebcb4843b9bca1e79e7436 - -FROM ${NODE_BUILD_IMAGE} AS development-dependencies +FROM node:10.24.1 +LABEL app="Community App" version="1.0" +RUN useradd -m -s /bin/bash appuser WORKDIR /opt/app - -# Native build tools stay isolated in the disposable builder stages. The -# Alpine runtime stage below never receives them. -RUN apk add --no-cache git python3 make g++ \ - && git config --global url."https://github.com/".insteadOf "git://github.com/" - -COPY package.json package-lock.json .npmrc ./ -COPY vendor ./vendor - -RUN npm ci - -FROM development-dependencies AS test - -ENV CI=true - COPY . . -RUN npm test - -FROM test AS build +RUN chown -R appuser:appuser /opt/app +USER appuser -ARG CDN_URL -ARG NODE_CONFIG_ENV=production - -ENV BABEL_ENV=production \ - CDN_URL=${CDN_URL} \ - NODE_CONFIG_ENV=${NODE_CONFIG_ENV} \ - NODE_ENV=production - -# The browser bundle is built as before. Server/shared sources are then -# precompiled so the runtime does not need Babel or the Webpack toolchain. -RUN npm run build \ - && ./node_modules/.bin/babel src \ - --out-dir /opt/runtime-src \ - --copy-files \ - --extensions ".js,.jsx" \ - && rm -rf \ - /opt/runtime-src/client \ - /opt/runtime-src/styles \ - /opt/runtime-src/test \ - && find /opt/runtime-src -type f \ - ! -name "*.js" \ - ! -name "*.json" \ - -delete \ - && install --directory /opt/runtime-src/assets/images \ - && install --mode=0644 \ - src/assets/images/favicon.ico \ - /opt/runtime-src/assets/images/favicon.ico - -FROM development-dependencies AS production-dependencies - -ENV NODE_ENV=production - -RUN npm prune --omit=dev --ignore-scripts \ - && npm cache clean --force - -FROM ${NODE_IMAGE} AS runtime - -LABEL org.opencontainers.image.title="Topcoder Community App" \ - org.opencontainers.image.description="Topcoder Community App web server" +################################################################################ +# Receiving of build arguments. +ARG AUTH0_CLIENT_ID ARG CDN_URL -ARG NODE_CONFIG_ENV=production - -ENV BABEL_ENV=production \ - CDN_URL=${CDN_URL} \ - NODE_CONFIG_ENV=${NODE_CONFIG_ENV} \ - NODE_ENV=production \ - PORT=3000 - -WORKDIR /opt/app - -# The application starts Node directly, so package-manager executables and -# their dependency trees are unnecessary attack surface in production. -RUN rm -rf \ - /opt/yarn-* \ - /usr/local/lib/node_modules/corepack \ - /usr/local/lib/node_modules/npm \ - && rm -f \ - /usr/local/bin/corepack \ - /usr/local/bin/npm \ - /usr/local/bin/npx \ - /usr/local/bin/yarn \ - /usr/local/bin/yarnpkg - -COPY --from=production-dependencies --chown=node:node /opt/app/vendor ./vendor -COPY --from=production-dependencies --chown=node:node /opt/app/node_modules ./node_modules -COPY --from=build --chown=node:node /opt/app/build ./build -COPY --from=build --chown=node:node /opt/app/.build-info ./.build-info -COPY --from=build --chown=node:node /opt/runtime-src ./src -COPY --from=build --chown=node:node \ - /opt/app/config/custom-environment-variables.js \ - /opt/app/config/default.js \ - /opt/app/config/development.js \ - /opt/app/config/production.js \ - /opt/app/config/qa.js \ - ./config/ -COPY --from=build --chown=node:node /opt/app/config/contentful ./config/contentful -COPY --chown=node:node package.json ./package.json -COPY --chown=node:node bin/runtime.js ./bin/runtime.js - -# Parameter Store supplies the default Contentful delivery configuration during -# deployment builds. Declare it after all filesystem layers so secret rotation -# does not invalidate them or expose the token to any RUN instruction. +ARG COGNITIVE_NEWSLETTER_SIGNUP_APIKEY +ARG COGNITIVE_NEWSLETTER_SIGNUP_URL ARG CONTENTFUL_CDN_API_KEY +ARG CONTENTFUL_PREVIEW_API_KEY ARG CONTENTFUL_SPACE_ID -ENV CONTENTFUL_CDN_API_KEY=${CONTENTFUL_CDN_API_KEY} \ - CONTENTFUL_SPACE_ID=${CONTENTFUL_SPACE_ID} -USER node +# Credentials for access to Zurich space in Contentful CMS +ARG CONTENTFUL_ZURICH_SPACE_ID +ARG CONTENTFUL_ZURICH_CDN_API_KEY +ARG CONTENTFUL_ZURICH_PREVIEW_API_KEY + +# Credentials for access to TopGear space in Contentful CMS +ARG CONTENTFUL_TOPGEAR_SPACE_ID +ARG CONTENTFUL_TOPGEAR_CDN_API_KEY +ARG CONTENTFUL_TOPGEAR_PREVIEW_API_KEY + +# Credentials for access to Comcast space in Contentful CMS +ARG CONTENTFUL_COMCAST_SPACE_ID +ARG CONTENTFUL_COMCAST_CDN_API_KEY +ARG CONTENTFUL_COMCAST_PREVIEW_API_KEY + +#Credentials for Contentfu EDU space + +ARG CONTENTFUL_MANAGEMENT_TOKEN +ARG CONTENTFUL_EDU_SPACE_ID +ARG CONTENTFUL_EDU_CDN_API_KEY +ARG CONTENTFUL_EDU_PREVIEW_API_KEY + +ARG FILESTACK_API_KEY +ARG FILESTACK_SUBMISSION_CONTAINER +ARG RECRUITCRM_API_KEY + +# Credentials for Mailchimp service +ARG MAILCHIMP_API_KEY +ARG MAILCHIMP_BASE_URL + +ARG NODE_CONFIG_ENV +ARG OPEN_EXCHANGE_RATES_KEY +ARG SEGMENT_IO_API_KEY +ARG CHAMELEON_VERIFICATION_SECRET +ARG SERVER_API_KEY + +# TC M2M credentials for Community App server +ARG TC_M2M_CLIENT_ID +ARG TC_M2M_CLIENT_SECRET +ARG TC_M2M_AUDIENCE +ARG TC_M2M_GRANT_TYPE + +ARG TC_M2M_AUTH0_PROXY_SERVER_URL +ARG TC_M2M_AUTH0_URL +ARG AUTH_SECRET +ARG VALID_ISSUERS + +ARG COMMUNITY_APP_URL +ARG GSHEETS_API_KEY + +# Gig work referrals +ARG SENDGRID_API_KEY +ARG GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY + +# Optimizely +ARG OPTIMIZELY_SDK_KEY + +# Gamification +ARG GAMIFICATION_ORG_ID + +# Universal Nav +ARG UNIVERSAL_NAV_URL + +# Topgear submissions allowed domains +ARG TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS + +################################################################################ +# Setting of environment variables in the Docker image. + +ENV AUTH0_CLIENT_ID=$AUTH0_CLIENT_ID +ENV CDN_URL=$CDN_URL +ENV COGNITIVE_NEWSLETTER_SIGNUP_APIKEY=$COGNITIVE_NEWSLETTER_SIGNUP_APIKEY +ENV COGNITIVE_NEWSLETTER_SIGNUP_URL=$COGNITIVE_NEWSLETTER_SIGNUP_URL +ENV CONTENTFUL_CDN_API_KEY=$CONTENTFUL_CDN_API_KEY +ENV CONTENTFUL_PREVIEW_API_KEY=$CONTENTFUL_PREVIEW_API_KEY +ENV CONTENTFUL_SPACE_ID=$CONTENTFUL_SPACE_ID + +# Credentials for access to Zurich space in Contentful CMS +ENV CONTENTFUL_ZURICH_SPACE_ID=$CONTENTFUL_ZURICH_SPACE_ID +ENV CONTENTFUL_ZURICH_CDN_API_KEY=$CONTENTFUL_ZURICH_CDN_API_KEY +ENV CONTENTFUL_ZURICH_PREVIEW_API_KEY=$CONTENTFUL_ZURICH_PREVIEW_API_KEY + +# Credentials for access to TopGear space in Contentful CMS +ENV CONTENTFUL_TOPGEAR_SPACE_ID=$CONTENTFUL_TOPGEAR_SPACE_ID +ENV CONTENTFUL_TOPGEAR_CDN_API_KEY=$CONTENTFUL_TOPGEAR_CDN_API_KEY +ENV CONTENTFUL_TOPGEAR_PREVIEW_API_KEY=$CONTENTFUL_TOPGEAR_PREVIEW_API_KEY + +# Credentials for access to Comcast space in Contentful CMS +ENV CONTENTFUL_COMCAST_SPACE_ID=$CONTENTFUL_COMCAST_SPACE_ID +ENV CONTENTFUL_COMCAST_CDN_API_KEY=$CONTENTFUL_COMCAST_CDN_API_KEY +ENV CONTENTFUL_COMCAST_PREVIEW_API_KEY=$CONTENTFUL_COMCAST_PREVIEW_API_KEY + +ENV FILESTACK_API_KEY=$FILESTACK_API_KEY +ENV FILESTACK_SUBMISSION_CONTAINER=$FILESTACK_SUBMISSION_CONTAINER + +# Credentials for Mailchimp service +ENV MAILCHIMP_API_KEY=$MAILCHIMP_API_KEY +ENV MAILCHIMP_BASE_URL=$MAILCHIMP_BASE_URL + +ENV NODE_CONFIG_ENV=$NODE_CONFIG_ENV +ENV OPEN_EXCHANGE_RATES_KEY=$OPEN_EXCHANGE_RATES_KEY +ENV SEGMENT_IO_API_KEY=$SEGMENT_IO_API_KEY +ENV CHAMELEON_VERIFICATION_SECRET=$CHAMELEON_VERIFICATION_SECRET +ENV SERVER_API_KEY=$SERVER_API_KEY + +# TC M2M credentials for Community App server +ENV TC_M2M_CLIENT_ID=$TC_M2M_CLIENT_ID +ENV TC_M2M_CLIENT_SECRET=$TC_M2M_CLIENT_SECRET +ENV TC_M2M_AUDIENCE=$TC_M2M_AUDIENCE +ENV TC_M2M_GRANT_TYPE=$TC_M2M_GRANT_TYPE + +ENV TC_M2M_AUTH0_PROXY_SERVER_URL=$TC_M2M_AUTH0_PROXY_SERVER_URL +ENV TC_M2M_AUTH0_URL=$TC_M2M_AUTH0_URL +ENV AUTH_SECRET=$AUTH_SECRET +ENV VALID_ISSUERS=$VALID_ISSUERS + +ENV CONTENTFUL_MANAGEMENT_TOKEN=$CONTENTFUL_MANAGEMENT_TOKEN +ENV CONTENTFUL_EDU_SPACE_ID=$CONTENTFUL_EDU_SPACE_ID +ENV CONTENTFUL_EDU_CDN_API_KEY=$CONTENTFUL_EDU_CDN_API_KEY +ENV CONTENTFUL_EDU_PREVIEW_API_KEY=$CONTENTFUL_EDU_PREVIEW_API_KEY +ENV RECRUITCRM_API_KEY=$RECRUITCRM_API_KEY +ENV COMMUNITY_APP_URL=$COMMUNITY_APP_URL +ENV SENDGRID_API_KEY=$SENDGRID_API_KEY +ENV GSHEETS_API_KEY=$GSHEETS_API_KEY +ENV GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY=$GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY + +# Optimizely +ENV OPTIMIZELY_SDK_KEY=$OPTIMIZELY_SDK_KEY + +ENV GAMIFICATION_ORG_ID=$GAMIFICATION_ORG_ID + +# Universal nav +ENV UNIVERSAL_NAV_URL=$UNIVERSAL_NAV_URL + +# Topgear submissions allowed domains +ENV TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS=$TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS + +################################################################################ +# Testing and build of the application inside the container. + +RUN npm config set unsafe-perm true +RUN git config --global url."https://git@".insteadOf git:// +RUN npm ci +RUN npm test +RUN npm run build EXPOSE 3000 - -HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ - CMD ["node", "-e", "const http=require('http');const req=http.get({host:'127.0.0.1',port:process.env.PORT||3000,path:'/api/cdn/public/ping',timeout:3000},res=>{res.resume();process.exit(res.statusCode===200?0:1);});req.on('timeout',()=>{req.destroy();process.exit(1);});req.on('error',()=>process.exit(1));"] - -STOPSIGNAL SIGTERM - -CMD ["node", "--max-old-space-size=8192", "bin/runtime.js"] +CMD ["npm", "start"] diff --git a/__tests__/.eslintrc b/__tests__/.eslintrc index f2d3a19d7..c4c8a8486 100644 --- a/__tests__/.eslintrc +++ b/__tests__/.eslintrc @@ -1,12 +1,3 @@ { - "env": { - "jest": true - }, - "plugins": [ - "jest" - ], - "rules": { - "global-require": 0, - "import/no-dynamic-require": 0 - } -} + "extends": "../node_modules/topcoder-react-utils/config/eslint/jest.json" +} \ No newline at end of file diff --git a/__tests__/config/css-modules.js b/__tests__/config/css-modules.js deleted file mode 100644 index e87e6410c..000000000 --- a/__tests__/config/css-modules.js +++ /dev/null @@ -1,27 +0,0 @@ -/* eslint-env jest */ - -const path = require('path'); -const { generateScopedName } = require('../../config/css-modules'); - -test('production CSS module names are valid and match css-loader identifiers', () => { - const originalBabelEnv = process.env.BABEL_ENV; - process.env.BABEL_ENV = 'production'; - - try { - const topcoderStyles = path.resolve( - __dirname, - '../../src/shared/routes/Topcoder/styles.scss', - ); - const listingStyles = path.resolve( - __dirname, - '../../src/shared/containers/challenge-listing/Listing/styles.scss', - ); - - expect(generateScopedName('container', topcoderStyles)).toBe('_10hiPS'); - expect(generateScopedName('container', listingStyles)).toBe('_3pMa6m'); - expect(generateScopedName('bannerContent', listingStyles)).toBe('JwBbj_'); - } finally { - if (originalBabelEnv === undefined) delete process.env.BABEL_ENV; - else process.env.BABEL_ENV = originalBabelEnv; - } -}); diff --git a/__tests__/config/security.js b/__tests__/config/security.js deleted file mode 100644 index 56c07e95f..000000000 --- a/__tests__/config/security.js +++ /dev/null @@ -1,180 +0,0 @@ -/* eslint-env jest */ - -const fs = require('fs'); -const nodePath = require('path'); - -const backupDefaults = require('../../config/backup-default'); -const customEnvironmentVariables = require('../../config/custom-environment-variables'); -const defaults = require('../../config/default'); -const development = require('../../config/development'); -const production = require('../../config/production'); -const qa = require('../../config/qa'); -const testConfig = require('../../config/test'); - -/** - * Gets a nested configuration value from a dot-separated path. - * - * @param {Object} object configuration object - * @param {String} path dot-separated configuration path - * @returns {*} resolved value - */ -function get(object, path) { - return path.split('.').reduce((value, key) => value && value[key], object); -} - -const DEFAULT_CREDENTIAL_PATHS = [ - 'LOG_ENTRIES_TOKEN', - 'NEWSLETTER_SIGNUP.COGNITIVE.APIKEY', - 'SEGMENT_IO_API_KEY', - 'SERVER_API_KEY', - 'FILESTACK.API_KEY', - 'SECRET.CONTENTFUL.MANAGEMENT_TOKEN', - 'SECRET.CONTENTFUL.default.master.CDN_API_KEY', - 'SECRET.CONTENTFUL.default.master.PREVIEW_API_KEY', - 'SECRET.CONTENTFUL.EDU.master.CDN_API_KEY', - 'SECRET.CONTENTFUL.EDU.master.PREVIEW_API_KEY', - 'SECRET.CONTENTFUL.zurich.master.CDN_API_KEY', - 'SECRET.CONTENTFUL.zurich.master.PREVIEW_API_KEY', - 'SECRET.CONTENTFUL.topgear.master.CDN_API_KEY', - 'SECRET.CONTENTFUL.topgear.master.PREVIEW_API_KEY', - 'SECRET.CONTENTFUL.comcast.master.CDN_API_KEY', - 'SECRET.CONTENTFUL.comcast.master.PREVIEW_API_KEY', - 'SECRET.MAILCHIMP.default.API_KEY', - 'SECRET.OPEN_EXCHANGE_RATES_KEY', - 'SECRET.TC_M2M.CLIENT_ID', - 'SECRET.TC_M2M.CLIENT_SECRET', - 'SECRET.RECRUITCRM_API_KEY', - 'SECRET.SENDGRID_API_KEY', - 'SECRET.JWT_AUTH.SECRET', - 'SECRET.JWT_AUTH.AUTH_SECRET', - 'SECRET.CHAMELEON_VERIFICATION_SECRET', - 'GSHEETS_API_KEY', - 'GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY', -]; - -const ENVIRONMENT_CREDENTIAL_PATHS = [ - ['development', development, 'SEGMENT_IO_API_KEY'], - ['development', development, 'SERVER_API_KEY'], - ['production', production, 'LOG_ENTRIES_TOKEN'], - ['production', production, 'SERVER_API_KEY'], - ['qa', qa, 'SEGMENT_IO_API_KEY'], - ['qa', qa, 'SERVER_API_KEY'], - ['test', testConfig, 'SERVER_API_KEY'], -]; - -describe('credential configuration', () => { - [ - ['default', defaults], - ['backup-default', backupDefaults], - ].forEach(([name, config]) => { - test(`${name} configuration has no credential fallback values`, () => { - DEFAULT_CREDENTIAL_PATHS.forEach((path) => { - expect(get(config, path)).toBe(''); - }); - }); - }); - - ENVIRONMENT_CREDENTIAL_PATHS.forEach(([name, config, path]) => { - test(`${name} configuration leaves ${path} empty`, () => { - expect(get(config, path)).toBe(''); - }); - }); - - test.each(DEFAULT_CREDENTIAL_PATHS)( - '%s is mapped to an environment variable', - (path) => { - expect(get(customEnvironmentVariables, path)).toMatch(/^[A-Z0-9_]+$/); - }, - ); - - test('Docker builds receive only approved build arguments', () => { - const source = fs.readFileSync( - nodePath.resolve(__dirname, '../../build.sh'), - 'utf8', - ); - const buildArguments = [...source.matchAll(/--build-arg\s+["']?([A-Z0-9_]+)/g)] - .map(match => match[1]); - - expect(buildArguments).toEqual([ - 'CDN_URL', - 'CONTENTFUL_CDN_API_KEY', - 'CONTENTFUL_SPACE_ID', - 'NODE_CONFIG_ENV', - ]); - expect(source).toContain( - [ - ': "$', - '{CONTENTFUL_CDN_API_KEY:?CONTENTFUL_CDN_API_KEY must be set by the build environment}"', - ].join(''), - ); - expect(source).toContain( - [ - ': "$', - '{CONTENTFUL_SPACE_ID:?CONTENTFUL_SPACE_ID must be set by the build environment}"', - ].join(''), - ); - }); - - test('runtime image preserves default Contentful delivery build arguments', () => { - const source = fs.readFileSync( - nodePath.resolve(__dirname, '../../Dockerfile'), - 'utf8', - ); - const runtimeMarker = ['FROM $', '{NODE_IMAGE} AS runtime'].join(''); - const runtimeStart = source.indexOf(runtimeMarker); - const buildStage = source.slice(0, runtimeStart); - const runtimeStage = source.slice(runtimeStart); - const contentfulArgumentsStart = runtimeStage.indexOf('ARG CONTENTFUL_CDN_API_KEY'); - - expect(runtimeStart).toBeGreaterThan(-1); - expect(buildStage).not.toContain('CONTENTFUL_CDN_API_KEY'); - expect(buildStage).not.toContain('CONTENTFUL_SPACE_ID'); - expect(contentfulArgumentsStart).toBeGreaterThan(runtimeStage.lastIndexOf('COPY ')); - expect(runtimeStage).toContain('ARG CONTENTFUL_CDN_API_KEY'); - expect(runtimeStage).toContain('ARG CONTENTFUL_SPACE_ID'); - expect(runtimeStage).toContain( - ['CONTENTFUL_CDN_API_KEY=$', '{CONTENTFUL_CDN_API_KEY}'].join(''), - ); - expect(runtimeStage).toContain( - ['CONTENTFUL_SPACE_ID=$', '{CONTENTFUL_SPACE_ID}'].join(''), - ); - }); - - test('JMeter loads M2M credentials from runtime properties', () => { - const source = fs.readFileSync( - nodePath.resolve(__dirname, '../../src/test/jmeter/Community-25UV.jmx'), - 'utf8', - ); - - // eslint-disable-next-line no-template-curly-in-string - expect(source).toContain('${__P(TC_M2M_CLIENT_ID,)}'); - // eslint-disable-next-line no-template-curly-in-string - expect(source).toContain('${__P(TC_M2M_CLIENT_SECRET,)}'); - expect(source).not.toMatch( - /name="client_(?:id|secret)"[\s\S]{0,250}Argument\.value">(?!(?:\$\{__P\(|<\/))/, - ); - }); - - test('Segment analytics uses the configured key without a static literal', () => { - const source = fs.readFileSync( - nodePath.resolve(__dirname, '../../src/server/index.js'), - 'utf8', - ); - - expect(source).toMatch( - /analytics\.load\(\$\{serializeJs\(config\.SEGMENT_IO_API_KEY\)\}\);/, - ); - expect(source).not.toMatch(/analytics\.load\(['"][^'"]+['"]\)/); - }); - - test('API-key authorization fails closed when configuration is empty', () => { - const source = fs.readFileSync( - nodePath.resolve(__dirname, '../../src/server/index.js'), - 'utf8', - ); - - expect(source).toMatch( - /if \(!config\.SERVER_API_KEY\s*\|\|\s*req\.headers\.authorization !==/, - ); - }); -}); diff --git a/__tests__/config/uninav.js b/__tests__/config/uninav.js deleted file mode 100644 index 7a51e9720..000000000 --- a/__tests__/config/uninav.js +++ /dev/null @@ -1,35 +0,0 @@ -/* eslint-env jest */ - -const { execFileSync } = require('child_process'); -const path = require('path'); - -const PROJECT_ROOT = path.resolve(__dirname, '../..'); - -test('UniNav components render in the plain Node production runtime', () => { - const script = ` - const React = require('react'); - const ReactDOMServer = require('react-dom/server'); - const { MarketingNavigation, ToolNavigation } = require('uninav-react'); - - const components = { MarketingNavigation, ToolNavigation }; - for (const [name, Component] of Object.entries(components)) { - if (typeof Component !== 'function') { - throw new TypeError(name + ' is not a React component'); - } - ReactDOMServer.renderToString(React.createElement(Component, { - currentLocation: '/', - toolName: 'Topcoder', - })); - } - `; - - expect(() => execFileSync(process.execPath, ['-e', script], { - cwd: PROJECT_ROOT, - env: { - ...process.env, - BABEL_ENV: 'production', - NODE_ENV: 'production', - }, - stdio: 'pipe', - })).not.toThrow(); -}); diff --git a/__tests__/config/webpack.js b/__tests__/config/webpack.js deleted file mode 100644 index 262e850ff..000000000 --- a/__tests__/config/webpack.js +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-env jest */ - -const crypto = require('crypto'); -const fs = require('fs'); -const forge = require('node-forge'); - -jest.unmock('webpack'); - -const createWebpackConfig = require('../../config/webpack/create'); - -describe('webpack build information', () => { - afterEach(() => { - jest.restoreAllMocks(); - }); - - test('generates a raw 32-byte key accepted by the renderer cipher', () => { - const bytes = [ - 0, 10, 13, 34, 92, 255, - ...Array.from({ length: 26 }, (_, index) => index + 128), - ]; - jest.spyOn(crypto, 'randomBytes').mockReturnValue(Buffer.from(bytes)); - const writeFile = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); - - const webpackConfig = createWebpackConfig('production'); - - const buildInfo = JSON.parse(writeFile.mock.calls[0][1]); - const definePlugin = webpackConfig.plugins.find(plugin => ( - plugin.definitions && plugin.definitions.BUILD_INFO - )); - const clientBuildInfo = JSON.parse(definePlugin.definitions.BUILD_INFO); - expect(buildInfo.key).toHaveLength(32); - expect([...buildInfo.key].map(character => character.charCodeAt(0))).toEqual(bytes); - expect(clientBuildInfo.key).toBe(buildInfo.key); - expect(() => forge.cipher.createCipher('AES-CBC', buildInfo.key)).not.toThrow(); - expect(() => forge.cipher.createDecipher('AES-CBC', clientBuildInfo.key)).not.toThrow(); - }); -}); diff --git a/__tests__/server/avatar.js b/__tests__/server/avatar.js deleted file mode 100644 index fd77a7354..000000000 --- a/__tests__/server/avatar.js +++ /dev/null @@ -1,101 +0,0 @@ -import fetch from 'isomorphic-fetch'; -import sharp from 'sharp'; - -import getAvatar, { normalizeAvatarUrl } from 'server/services/avatar'; - -jest.mock('isomorphic-fetch', () => jest.fn()); -jest.mock('sharp', () => jest.fn()); - -function mockResponse({ - body = Buffer.from('image'), - contentType = 'image/png', - location, - status = 200, -} = {}) { - const headers = { - 'content-length': String(body.length), - 'content-type': contentType, - location, - }; - return { - buffer: jest.fn(() => Promise.resolve(body)), - headers: { - get: jest.fn(name => headers[name.toLowerCase()] || null), - }, - ok: status >= 200 && status < 300, - status, - }; -} - -describe('avatar service security boundaries', () => { - let resize; - let toBuffer; - - beforeEach(() => { - jest.clearAllMocks(); - toBuffer = jest.fn(() => Promise.resolve(Buffer.from('resized'))); - resize = jest.fn(() => ({ toBuffer })); - sharp.mockReturnValue({ resize }); - }); - - test('rejects loopback and attacker-controlled destinations before fetching', async () => { - await expect(getAvatar('http://127.0.0.1/latest/meta-data', 32)) - .rejects.toThrow('Avatar URL is not trusted'); - await expect(getAvatar('https://member-media.topcoder.com.attacker.test/a.png', 32)) - .rejects.toThrow('Avatar URL is not trusted'); - - expect(fetch).not.toHaveBeenCalled(); - }); - - test('normalizes only legacy relative paths against the configured site', () => { - const normalized = normalizeAvatarUrl('/i/m/avatar.png'); - - expect(normalized.pathname).toBe('/i/m/avatar.png'); - expect(normalized.protocol).toBe('https:'); - }); - - test('fetches and resizes a bounded raster image from a trusted media host', async () => { - fetch.mockResolvedValue(mockResponse()); - - await expect(getAvatar( - 'https://topcoder-prod-media.s3.amazonaws.com/member/profile/avatar.png', - 64, - )).resolves.toEqual(Buffer.from('resized')); - - expect(fetch).toHaveBeenCalledWith( - 'https://topcoder-prod-media.s3.amazonaws.com/member/profile/avatar.png', - expect.objectContaining({ redirect: 'manual' }), - ); - expect(sharp).toHaveBeenCalledWith( - Buffer.from('image'), - { limitInputPixels: 40000000 }, - ); - expect(resize).toHaveBeenCalledWith(64, 64, { fit: 'inside' }); - }); - - test('rejects a redirect that leaves the trusted media origins', async () => { - fetch.mockResolvedValue(mockResponse({ - location: 'http://169.254.169.254/latest/meta-data', - status: 302, - })); - - await expect(getAvatar( - 'https://member-media.topcoder.com/avatar.png', - 32, - )).rejects.toThrow('Avatar URL is not trusted'); - expect(fetch).toHaveBeenCalledTimes(1); - }); - - test('rejects unsupported content types and excessive resize requests', async () => { - fetch.mockResolvedValue(mockResponse({ contentType: 'text/html' })); - - await expect(getAvatar( - 'https://member-media.topcoder.com/avatar.png', - 32, - )).rejects.toThrow('Avatar response is not a supported image'); - await expect(getAvatar( - 'https://member-media.topcoder.com/avatar.png', - 2048, - )).rejects.toThrow('Invalid avatar size'); - }); -}); diff --git a/__tests__/server/contentful.js b/__tests__/server/contentful.js deleted file mode 100644 index c8a08480e..000000000 --- a/__tests__/server/contentful.js +++ /dev/null @@ -1,65 +0,0 @@ -import config from 'config'; -import { createClient } from 'contentful'; - -import { getService } from 'server/services/contentful'; - -jest.mock('contentful', () => ({ - createClient: jest.fn(() => ({})), -})); -jest.mock('contentful-management', () => ({ - createClient: jest.fn(), -})); -jest.mock('topcoder-react-lib', () => ({ - logger: { log: jest.fn() }, -})); -jest.mock('topcoder-react-utils', () => ({ - isomorphy: { isDev: false }, -})); - -describe('server Contentful service configuration', () => { - const originalDefault = config.SECRET.CONTENTFUL.default; - const originalUnrelated = config.SECRET.CONTENTFUL.unrelated; - - beforeAll(() => { - config.SECRET.CONTENTFUL.default = { - SPACE_ID: 'default-space', - master: { - CDN_API_KEY: 'default-cdn-token', - PREVIEW_API_KEY: '', - }, - }; - config.SECRET.CONTENTFUL.unrelated = { - SPACE_ID: '', - master: { - CDN_API_KEY: '', - PREVIEW_API_KEY: '', - }, - }; - }); - - afterAll(() => { - config.SECRET.CONTENTFUL.default = originalDefault; - if (originalUnrelated) { - config.SECRET.CONTENTFUL.unrelated = originalUnrelated; - } else { - delete config.SECRET.CONTENTFUL.unrelated; - } - }); - - test('creates and caches only the requested delivery client', () => { - const first = getService('default', 'master', false); - const second = getService('default', 'master', false); - - expect(first).toBe(second); - expect(createClient).toHaveBeenCalledTimes(1); - expect(createClient).toHaveBeenCalledWith(expect.objectContaining({ - accessToken: 'default-cdn-token', - space: 'default-space', - })); - expect(createClient.mock.calls[0][0].host).toBeUndefined(); - - expect(() => getService('unrelated', 'master', false)) - .toThrow(/unrelated.*CONTENTFUL_UNRELATED_SPACE_ID.*CONTENTFUL_UNRELATED_CDN_API_KEY/); - expect(createClient).toHaveBeenCalledTimes(1); - }); -}); diff --git a/__tests__/server/recruitCRM.js b/__tests__/server/recruitCRM.js deleted file mode 100644 index 5b2b01aaa..000000000 --- a/__tests__/server/recruitCRM.js +++ /dev/null @@ -1,96 +0,0 @@ -import fetch from 'isomorphic-fetch'; - -import RecruitCRMService, { - normalizeRecruitCrmIdentifier, - parseApplicationForm, -} from 'server/services/recruitCRM'; - -jest.mock('isomorphic-fetch', () => jest.fn()); -jest.mock('topcoder-react-lib', () => ({ - logger: { - error: jest.fn(), - }, - services: { - api: {}, - }, -})); -jest.mock('server/services/sendGrid', () => ({ - sendEmailDirect: jest.fn(), -})); - -function validApplication(overrides = {}) { - return { - city: 'Hobart', - contact_number: '+61 400 000 000', - custom_fields: [ - { field_id: 1, value: 'https://topcoder.com/members/member' }, - { field_id: 2, value: 'member' }, - { field_id: 14, value: 'Job information' }, - ], - email: 'member@example.com', - first_name: 'Test', - last_name: 'Member', - locality: 'Australia', - salary_expectation: '', - skill: 'JavaScript', - ...overrides, - }; -} - -describe('RecruitCRM input security boundaries', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - test('accepts bounded opaque identifiers and rejects URL path injection', () => { - expect(normalizeRecruitCrmIdentifier('job_slug-123')).toBe('job_slug-123'); - expect(normalizeRecruitCrmIdentifier('../../admin')).toBeNull(); - expect(normalizeRecruitCrmIdentifier('job/assign?admin=true')).toBeNull(); - expect(normalizeRecruitCrmIdentifier('short')).toBeNull(); - }); - - test('parses a valid bounded application form', () => { - const parsed = parseApplicationForm(JSON.stringify(validApplication())); - - expect(parsed.email).toBe('member@example.com'); - expect(parsed.custom_fields).toHaveLength(3); - }); - - test('rejects malformed and unbounded custom field arrays', () => { - expect(() => parseApplicationForm('{invalid-json')) - .toThrow('Invalid application form'); - expect(() => parseApplicationForm(JSON.stringify(validApplication({ - custom_fields: Array.from( - { length: 33 }, - (value, fieldId) => ({ field_id: fieldId + 1, value: '' }), - ), - })))).toThrow('Invalid application form'); - }); - - test('rejects an invalid job identifier before any upstream request', async () => { - const service = new RecruitCRMService(); - const req = { - body: { form: JSON.stringify(validApplication()) }, - params: { id: '../../../metadata' }, - }; - const res = { - json: jest.fn(), - status: jest.fn(), - }; - res.status.mockReturnValue(res); - - await service.applyForJob(req, res, jest.fn()); - - expect(res.status).toHaveBeenCalledWith(400); - expect(res.json).toHaveBeenCalledWith({ error: 'Invalid job ID format.' }); - expect(fetch).not.toHaveBeenCalled(); - }); - - test('does not return an upstream exception or stack trace to the cache', async () => { - fetch.mockRejectedValue(new Error('private upstream stack details')); - const service = new RecruitCRMService(); - - await expect(service.getAll({ job_status: 1 })) - .resolves.toEqual({ error: true }); - }); -}); diff --git a/__tests__/server/routes/authentication.js b/__tests__/server/routes/authentication.js deleted file mode 100644 index d45b68850..000000000 --- a/__tests__/server/routes/authentication.js +++ /dev/null @@ -1,63 +0,0 @@ -import { createJwtAuthenticator } from 'server/routes/authentication'; - -function mockResponse() { - const res = { - json: jest.fn(), - status: jest.fn(), - }; - res.status.mockReturnValue(res); - return res; -} - -describe('JWT route configuration', () => { - test('fails closed when the canonical secret is absent', () => { - const factory = jest.fn(); - const handler = createJwtAuthenticator({ - AUTH_SECRET: 'legacy-fallback-must-not-be-used', - SECRET: '', - VALID_ISSUERS: '["https://api.topcoder.com"]', - }, factory); - const next = jest.fn(); - const res = mockResponse(); - - handler({}, res, next); - - expect(factory).not.toHaveBeenCalled(); - expect(next).not.toHaveBeenCalled(); - expect(res.status).toHaveBeenCalledWith(503); - expect(res.json).toHaveBeenCalledWith({ - error: 'Authentication is unavailable.', - }); - }); - - test('fails closed when issuer configuration is malformed', () => { - const factory = jest.fn(); - const handler = createJwtAuthenticator({ - SECRET: 'configured-secret', - VALID_ISSUERS: 'not-json', - }, factory); - const next = jest.fn(); - const res = mockResponse(); - - handler({}, res, next); - - expect(factory).not.toHaveBeenCalled(); - expect(next).not.toHaveBeenCalled(); - expect(res.status).toHaveBeenCalledWith(503); - }); - - test('maps the canonical secret to the tc-core authenticator contract', () => { - const expectedMiddleware = jest.fn(); - const factory = jest.fn(() => expectedMiddleware); - const handler = createJwtAuthenticator({ - SECRET: 'configured-secret', - VALID_ISSUERS: ['https://api.topcoder.com'], - }, factory); - - expect(handler).toBe(expectedMiddleware); - expect(factory).toHaveBeenCalledWith({ - AUTH_SECRET: 'configured-secret', - VALID_ISSUERS: '["https://api.topcoder.com"]', - }); - }); -}); diff --git a/__tests__/server/routes/security.js b/__tests__/server/routes/security.js deleted file mode 100644 index 9a8dfa1df..000000000 --- a/__tests__/server/routes/security.js +++ /dev/null @@ -1,98 +0,0 @@ -import config from 'config'; -import express from 'express'; -import request from 'supertest'; - -import { - configuredJwtAuthenticator, - protectedCorsOptions, -} from 'server/routes/authentication'; -import contentfulRoutes, { - articleVoteLimiter, -} from 'server/routes/contentful'; -import recruitRoutes, { - sensitiveRouteLimiter, -} from 'server/routes/recruitCRM'; - -jest.mock('tc-core-library-js', () => ({ - middleware: { - jwtAuthenticator: jest.fn(() => (req, res, next) => next()), - }, -})); - -jest.mock('server/services/contentful', () => ({ - ALLOWED_DOMAINS: [], - ASSETS_DOMAIN: 'assets.example.test', - IMAGES_DOMAIN: 'images.example.test', - articleVote: jest.fn(), - getService: jest.fn(), - getSpaceId: jest.fn(), -})); - -jest.mock('server/services/recruitCRM', () => jest.fn()); - -/** - * Gets middleware attached to a specific Express router method. - * @param {Function} router Express router. - * @param {String} path Route path. - * @param {String} method Lowercase HTTP method. - * @return {Function[]} Attached middleware functions. - */ -function getRouteMiddleware(router, path, method) { - const layer = router.stack.find(item => item.route - && item.route.path === path - && item.route.methods[method]); - return layer ? layer.route.stack.map(item => item.handle) : []; -} - -describe('authenticated route protections', () => { - test.each([ - ['/jobs/cache/flush', 'get'], - ['/jobs/:id/apply', 'post'], - ['/profile', 'get'], - ['/profile', 'post'], - ])('rate limits RecruitCRM %s %s before handling it', (path, method) => { - const routeMiddleware = getRouteMiddleware(recruitRoutes, path, method); - expect(routeMiddleware).toContain(sensitiveRouteLimiter); - expect(routeMiddleware).toContain(configuredJwtAuthenticator); - expect(routeMiddleware.indexOf(sensitiveRouteLimiter)) - .toBeLessThan(routeMiddleware.indexOf(configuredJwtAuthenticator)); - }); - - test('rate limits Contentful article voting', () => { - const routeMiddleware = getRouteMiddleware( - contentfulRoutes, - '/:spaceName/:environment/votes', - 'post', - ); - expect(routeMiddleware).toContain(articleVoteLimiter); - expect(routeMiddleware).toContain(configuredJwtAuthenticator); - expect(routeMiddleware.indexOf(articleVoteLimiter)) - .toBeLessThan(routeMiddleware.indexOf(configuredJwtAuthenticator)); - }); - - test('uses exact configured origins instead of reflecting any request origin', () => { - expect(protectedCorsOptions.origin).toContain(new URL(config.URL.BASE).origin); - expect(protectedCorsOptions.origin).not.toContain('*'); - expect(protectedCorsOptions.origin).not.toContain(true); - }); - - test('does not reflect an untrusted origin on the job application preflight', async () => { - const app = express(); - app.use(recruitRoutes); - const trustedOrigin = new URL(config.URL.BASE).origin; - - const trustedResponse = await request(app) - .options('/jobs/job_slug-123/apply') - .set('Origin', trustedOrigin) - .set('Access-Control-Request-Method', 'POST'); - const untrustedResponse = await request(app) - .options('/jobs/job_slug-123/apply') - .set('Origin', 'https://attacker.example') - .set('Access-Control-Request-Method', 'POST'); - - expect(trustedResponse.headers['access-control-allow-origin']) - .toBe(trustedOrigin); - expect(untrustedResponse.headers['access-control-allow-origin']) - .toBeUndefined(); - }); -}); diff --git a/__tests__/shared/components/ChallengeTile/fixtures/design.json b/__tests__/shared/components/ChallengeTile/__mocks__/design.json similarity index 100% rename from __tests__/shared/components/ChallengeTile/fixtures/design.json rename to __tests__/shared/components/ChallengeTile/__mocks__/design.json diff --git a/__tests__/shared/components/ChallengeTile/fixtures/develop.json b/__tests__/shared/components/ChallengeTile/__mocks__/develop.json similarity index 100% rename from __tests__/shared/components/ChallengeTile/fixtures/develop.json rename to __tests__/shared/components/ChallengeTile/__mocks__/develop.json diff --git a/__tests__/shared/components/ChallengeTile/fixtures/marathon.json b/__tests__/shared/components/ChallengeTile/__mocks__/marathon.json similarity index 100% rename from __tests__/shared/components/ChallengeTile/fixtures/marathon.json rename to __tests__/shared/components/ChallengeTile/__mocks__/marathon.json diff --git a/__tests__/shared/components/ChallengeTile/__snapshots__/index.jsx.snap b/__tests__/shared/components/ChallengeTile/__snapshots__/index.jsx.snap index 4beb7d514..04faa6f2d 100644 --- a/__tests__/shared/components/ChallengeTile/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/ChallengeTile/__snapshots__/index.jsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders design 1`] = `
({ - getService: jest.fn(), -})); - -describe('Contentful SearchBar URL construction', () => { - const input = '&role=admin'; - - test.each([ - ['Author', { author: input }], - ['Title', { title: input }], - ['All', { phrase: input }], - ['Tags', { tags: [input] }], - ])('encodes %s input as query data rather than DOM markup', (filter, expected) => { - const searchUrl = buildSearchUrl(filter, input); - const query = searchUrl.slice(searchUrl.indexOf('?') + 1); - - expect(qs.parse(query)).toEqual(expected); - expect(searchUrl).not.toContain(' { - const searchUrl = buildSearchUrl('Tags', 'JavaScript & Node.js'); - const query = searchUrl.slice(searchUrl.indexOf('?') + 1); - - expect(qs.parse(query)).toEqual({ tags: ['JavaScript & Node.js'] }); - }); -}); diff --git a/__tests__/shared/components/Contentful/Shape/__snapshots__/Shape.jsx.snap b/__tests__/shared/components/Contentful/Shape/__snapshots__/Shape.jsx.snap index f476d6b90..32a134509 100644 --- a/__tests__/shared/components/Contentful/Shape/__snapshots__/Shape.jsx.snap +++ b/__tests__/shared/components/Contentful/Shape/__snapshots__/Shape.jsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Matches shallow shapshot 1`] = `
`; diff --git a/__tests__/shared/components/Contentful/TracksTree/ChildListRow/__snapshots__/ChildListRow.jsx.snap b/__tests__/shared/components/Contentful/TracksTree/ChildListRow/__snapshots__/ChildListRow.jsx.snap index a0c3db194..be163816b 100644 --- a/__tests__/shared/components/Contentful/TracksTree/ChildListRow/__snapshots__/ChildListRow.jsx.snap +++ b/__tests__/shared/components/Contentful/TracksTree/ChildListRow/__snapshots__/ChildListRow.jsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Matches shallow shapshot 1`] = `
diff --git a/__tests__/shared/components/GUIKit/TextInput/__snapshots__/index.jsx.snap b/__tests__/shared/components/GUIKit/TextInput/__snapshots__/index.jsx.snap index a4fda570a..739f1a551 100644 --- a/__tests__/shared/components/GUIKit/TextInput/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/GUIKit/TextInput/__snapshots__/index.jsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Default render 1`] = `
@@ -102,7 +102,7 @@ exports[`Matches shallow shapshot 1`] = ` > @@ -156,7 +156,7 @@ exports[`Matches shallow shapshot 1`] = ` > @@ -210,7 +210,7 @@ exports[`Matches shallow shapshot 1`] = ` > diff --git a/__tests__/shared/components/Leaderboard/__snapshots__/Podium.jsx.snap b/__tests__/shared/components/Leaderboard/__snapshots__/Podium.jsx.snap index 22be825a5..c5a8a3f5e 100644 --- a/__tests__/shared/components/Leaderboard/__snapshots__/Podium.jsx.snap +++ b/__tests__/shared/components/Leaderboard/__snapshots__/Podium.jsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Matches shallow shapshot 1`] = `
@@ -68,7 +68,7 @@ exports[`Matches shallow shapshot 2`] = ` > diff --git a/__tests__/shared/components/Loader/__snapshots__/Loader.jsx.snap b/__tests__/shared/components/Loader/__snapshots__/Loader.jsx.snap index df549582c..38508a917 100644 --- a/__tests__/shared/components/Loader/__snapshots__/Loader.jsx.snap +++ b/__tests__/shared/components/Loader/__snapshots__/Loader.jsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Matches shallow shapshot 1`] = `

Your submission has been received and may undergo AI-assisted review during Submission phase. Results will be available for inspection in the review app and final evaluation occurs during Review phase. diff --git a/__tests__/shared/components/SubmissionManagement/__snapshots__/ScreeningStatus.jsx.snap b/__tests__/shared/components/SubmissionManagement/__snapshots__/ScreeningStatus.jsx.snap index 095418576..e0c4ffcd1 100644 --- a/__tests__/shared/components/SubmissionManagement/__snapshots__/ScreeningStatus.jsx.snap +++ b/__tests__/shared/components/SubmissionManagement/__snapshots__/ScreeningStatus.jsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Snapshot match 1`] = ` +

diff --git a/src/shared/components/ProfilePage/Activity/ActivityCard/styles.scss b/src/shared/components/ProfilePage/Activity/ActivityCard/styles.scss index 96ecd6179..e4cecf0d4 100644 --- a/src/shared/components/ProfilePage/Activity/ActivityCard/styles.scss +++ b/src/shared/components/ProfilePage/Activity/ActivityCard/styles.scss @@ -76,7 +76,6 @@ } .link-button { - background: transparent; border: 1.5px solid $listing-checkbox-green; border-radius: 24px; width: 35px; @@ -85,7 +84,6 @@ flex-direction: row; justify-content: center; margin-top: -2px; - padding: 0; &a.selected:hover { cursor: default; diff --git a/src/shared/components/ProfilePage/BadgesModal/achievementMap.js b/src/shared/components/ProfilePage/BadgesModal/achievementMap.js index 3d991db6e..7431352f7 100644 --- a/src/shared/components/ProfilePage/BadgesModal/achievementMap.js +++ b/src/shared/components/ProfilePage/BadgesModal/achievementMap.js @@ -583,7 +583,7 @@ export const getGroupAchievements = achievements => groupAchievements.map(group }), })).filter(group => group.specificAchievements[0] && group.specificAchievements[0].active); -/** + /** * Returns a copy of the base singleAchievements map updated with data * from api * diff --git a/src/shared/components/ProfilePage/Stats/DistributionGraph/index.jsx b/src/shared/components/ProfilePage/Stats/DistributionGraph/index.jsx index 5f4401217..57556aa73 100644 --- a/src/shared/components/ProfilePage/Stats/DistributionGraph/index.jsx +++ b/src/shared/components/ProfilePage/Stats/DistributionGraph/index.jsx @@ -110,7 +110,7 @@ export default class DistributionGraph extends React.Component { const { padding } = $scope.desktop ? desktopMeasurements : mobileMeasurements; if (!$scope.desktop) { w = DistributionGraph.getMobileWidthGrapthMeasurements(); - h = (w * 240) / 288.0; + h = w * 240 / 288.0; this.mobileWidth = w; } const totalW = w + padding.left + padding.right; diff --git a/src/shared/components/ProfilePage/Stats/HistoryGraph/index.jsx b/src/shared/components/ProfilePage/Stats/HistoryGraph/index.jsx index 15b18105d..8d398caf4 100644 --- a/src/shared/components/ProfilePage/Stats/HistoryGraph/index.jsx +++ b/src/shared/components/ProfilePage/Stats/HistoryGraph/index.jsx @@ -112,7 +112,7 @@ export default class HistoryGraph extends React.Component { const { padding } = $scope.desktop ? desktopMeasurements : mobileMeasurements; if (!$scope.desktop) { w = HistoryGraph.getMobileWidthGrapthMeasurements(); - h = (w * 240) / 288.0; + h = w * 240 / 288.0; this.mobileWidth = w; } // const totalH = h + padding.top + padding.bottom; diff --git a/src/shared/components/SecurityReminder/index.jsx b/src/shared/components/SecurityReminder/index.jsx index dc4af6409..a130315bb 100644 --- a/src/shared/components/SecurityReminder/index.jsx +++ b/src/shared/components/SecurityReminder/index.jsx @@ -1,5 +1,7 @@ /* eslint-disable jsx-a11y/no-noninteractive-tabindex */ /* eslint jsx-a11y/no-static-element-interactions:0 */ +/* global window */ + import React, { useState } from 'react'; import PT from 'prop-types'; import { Modal, PrimaryButton } from 'topcoder-react-ui-kit'; diff --git a/src/shared/components/SubmissionPage/FilestackFilePicker/index.jsx b/src/shared/components/SubmissionPage/FilestackFilePicker/index.jsx index 68abe0b3e..7a39b8ea2 100644 --- a/src/shared/components/SubmissionPage/FilestackFilePicker/index.jsx +++ b/src/shared/components/SubmissionPage/FilestackFilePicker/index.jsx @@ -14,7 +14,7 @@ import _ from 'lodash'; import React from 'react'; import PT from 'prop-types'; -import * as filestack from 'filestack-js'; +import { client as filestack } from 'filestack-react'; import { PrimaryButton } from 'topcoder-react-ui-kit'; import { config } from 'topcoder-react-utils'; import { errors } from 'topcoder-react-lib'; @@ -133,45 +133,14 @@ class FilestackFilePicker extends React.Component { this.setState({ inputUrl: e.target.value }); } - /** - * Checks that a submission is a credential-free HTTPS URL. - * @param {String} url Candidate submission URL. - * @return {Boolean} True when the URL has a safe structure. - */ /* eslint-disable class-methods-use-this */ isValidUrl(url) { - try { - const parsed = new URL(url); - return parsed.protocol === 'https:' - && Boolean(parsed.hostname) - && !parsed.username - && !parsed.password - && !parsed.port; - } catch (e) { - return false; - } + return /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(url); /* eslint-disable-line no-useless-escape */ } - /** - * Checks a submission URL against the configured exact hostname allowlist. - * @param {String} url Candidate submission URL. - * @return {Boolean} True when the URL uses an allowed hostname. - */ isDomainAllowed(url) { - try { - const parsed = new URL(url); - const allowedDomains = config.TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS - .split('|') - .map(domain => domain.trim().toLowerCase()) - .filter(Boolean); - return parsed.protocol === 'https:' - && !parsed.username - && !parsed.password - && !parsed.port - && allowedDomains.includes(parsed.hostname.toLowerCase()); - } catch (e) { - return false; - } + const domainReg = new RegExp(`^https?://(${config.TOPGEAR_ALLOWED_SUBMISSIONS_DOMAINS})/.+`); + return !!url.match(domainReg); } /** diff --git a/src/shared/components/TrackHomePages/HowToCompetePage/Header/index.jsx b/src/shared/components/TrackHomePages/HowToCompetePage/Header/index.jsx index bc468d002..008281699 100644 --- a/src/shared/components/TrackHomePages/HowToCompetePage/Header/index.jsx +++ b/src/shared/components/TrackHomePages/HowToCompetePage/Header/index.jsx @@ -1,22 +1,27 @@ /** * Header Component */ +/* eslint-disable react/no-danger */ import React from 'react'; import PT from 'prop-types'; -import renderMarkdown from 'utils/markdown'; +import showdown from 'showdown'; import YouTubeVideo from 'components/YouTubeVideo'; import './styles.scss'; +const converter = new showdown.Converter(); + const Header = ({ data }) => (

{data.title}

-
- {renderMarkdown(data.text)} -
+

{ data.table && ( @@ -52,9 +57,12 @@ const Header = ({ data }) => ( ) }

-
- {renderMarkdown(data.text)} -
+
{ data.table && ( diff --git a/src/shared/components/TrackHomePages/HowToCompetePage/QAComponent/index.jsx b/src/shared/components/TrackHomePages/HowToCompetePage/QAComponent/index.jsx index 5e548891e..a4ce5c148 100644 --- a/src/shared/components/TrackHomePages/HowToCompetePage/QAComponent/index.jsx +++ b/src/shared/components/TrackHomePages/HowToCompetePage/QAComponent/index.jsx @@ -1,12 +1,15 @@ /** * Question and Answer Component */ +/* eslint-disable react/no-danger */ import React from 'react'; import PT from 'prop-types'; -import renderMarkdown from 'utils/markdown'; +import showdown from 'showdown'; import './styles.scss'; +const converter = new showdown.Converter(); + class QAComponent extends React.Component { constructor(props) { super(props); @@ -40,9 +43,12 @@ class QAComponent extends React.Component {
-
- {renderMarkdown(data.text)} -
+
); } diff --git a/src/shared/components/TrackHomePages/HowToCompetePage/StepByStep/index.jsx b/src/shared/components/TrackHomePages/HowToCompetePage/StepByStep/index.jsx index 2adfa4b8b..41b9f132c 100644 --- a/src/shared/components/TrackHomePages/HowToCompetePage/StepByStep/index.jsx +++ b/src/shared/components/TrackHomePages/HowToCompetePage/StepByStep/index.jsx @@ -1,17 +1,20 @@ /** Step By Step Component */ +/* eslint-disable react/no-danger */ import _ from 'lodash'; import React from 'react'; import PT from 'prop-types'; +import showdown from 'showdown'; import Sticky from 'react-stickynode'; import ContentfulLoader from 'containers/ContentfulLoader'; -import renderMarkdown from 'utils/markdown'; import StepButton from './StepButton'; import './styles.scss'; +const converter = new showdown.Converter(); + class StepByStep extends React.Component { constructor(props) { super(props); @@ -69,9 +72,12 @@ class StepByStep extends React.Component {
{ menu.fields.title}
-
- {renderMarkdown(menu.fields.text)} -
+
)) diff --git a/src/shared/components/challenge-detail/Checkpoints/index.jsx b/src/shared/components/challenge-detail/Checkpoints/index.jsx index edc4e9b0b..71386b226 100644 --- a/src/shared/components/challenge-detail/Checkpoints/index.jsx +++ b/src/shared/components/challenge-detail/Checkpoints/index.jsx @@ -1,3 +1,4 @@ +/* global document */ import React from 'react'; import PT from 'prop-types'; diff --git a/src/shared/components/challenge-detail/Header/DeadlinesPanel/index.jsx b/src/shared/components/challenge-detail/Header/DeadlinesPanel/index.jsx index dcdddf38f..081d958d8 100644 --- a/src/shared/components/challenge-detail/Header/DeadlinesPanel/index.jsx +++ b/src/shared/components/challenge-detail/Header/DeadlinesPanel/index.jsx @@ -16,6 +16,7 @@ export default function DeadlinesPanel({ deadlines }) { const getCardProps = (deadline, index) => { let { name } = deadline; let showRange = true; + name = name.replace(/\bCheckpoint\b/, 'Checkpoint'); if (/.+submission/i.test(name)) { hasSubmissionPhase = true; name = name.replace(/submission/i, 'Submission'); diff --git a/src/shared/components/challenge-detail/Specification/SideBar/ShareSocial.jsx b/src/shared/components/challenge-detail/Specification/SideBar/ShareSocial.jsx index efc1b7235..7428212a7 100644 --- a/src/shared/components/challenge-detail/Specification/SideBar/ShareSocial.jsx +++ b/src/shared/components/challenge-detail/Specification/SideBar/ShareSocial.jsx @@ -1,5 +1,7 @@ /* global document window */ +/* eslint-disable jsx-a11y/href-no-hash */ + import React from 'react'; import TwitterIcon from '../../../../../assets/images/social/icon_twitter.svg'; diff --git a/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx b/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx index 734510d85..3fe0b2e9d 100644 --- a/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx +++ b/src/shared/components/challenge-listing/Filters/FiltersPanel/index.jsx @@ -1,4 +1,6 @@ /* eslint jsx-a11y/no-static-element-interactions:0 */ +/* global window */ + /** * Challenge filters panel. * diff --git a/src/shared/components/challenge-listing/Listing/Bucket/index.jsx b/src/shared/components/challenge-listing/Listing/Bucket/index.jsx index 9901b52cc..7d3cb2660 100644 --- a/src/shared/components/challenge-listing/Listing/Bucket/index.jsx +++ b/src/shared/components/challenge-listing/Listing/Bucket/index.jsx @@ -2,6 +2,8 @@ * A single bucket of challenges. */ +/* global document */ + import _ from 'lodash'; import PT from 'prop-types'; // import qs from 'qs'; diff --git a/src/shared/components/challenge-listing/placeholders/ChallengeCard/index.jsx b/src/shared/components/challenge-listing/placeholders/ChallengeCard/index.jsx index 6ed5e898e..8b272ac95 100644 --- a/src/shared/components/challenge-listing/placeholders/ChallengeCard/index.jsx +++ b/src/shared/components/challenge-listing/placeholders/ChallengeCard/index.jsx @@ -1,3 +1,7 @@ +/* global + Math +*/ + /** * The component displays a challenge card without any data. * The empty data is replaced with grey background. diff --git a/src/shared/components/examples/BlogFeed/index.jsx b/src/shared/components/examples/BlogFeed/index.jsx index 418f515cd..1aa90714c 100644 --- a/src/shared/components/examples/BlogFeed/index.jsx +++ b/src/shared/components/examples/BlogFeed/index.jsx @@ -2,7 +2,7 @@ import BlogFeedContainer from 'containers/Dashboard/BlogFeed'; import React from 'react'; import './styles.scss'; -import { PrimaryButton } from 'topcoder-react-ui-kit'; +import { PrimaryButton } from 'topcoder-react-ui-kit/src/shared/components/buttons'; export default class BlogFeedExample extends React.Component { constructor() { diff --git a/src/shared/components/examples/ChallengesFeed/index.jsx b/src/shared/components/examples/ChallengesFeed/index.jsx index 9c5db33f4..77171427c 100644 --- a/src/shared/components/examples/ChallengesFeed/index.jsx +++ b/src/shared/components/examples/ChallengesFeed/index.jsx @@ -2,7 +2,7 @@ import React from 'react'; import ChallengesFeed from 'containers/Dashboard/ChallengesFeed'; import './styles.scss'; -import { PrimaryButton } from 'topcoder-react-ui-kit'; +import { PrimaryButton } from 'topcoder-react-ui-kit/src/shared/components/buttons'; export default class ChallengesFeedExample extends React.Component { constructor() { diff --git a/src/shared/components/examples/CodeSplitting/index.jsx b/src/shared/components/examples/CodeSplitting/index.jsx index fe1f8e6ad..212678793 100644 --- a/src/shared/components/examples/CodeSplitting/index.jsx +++ b/src/shared/components/examples/CodeSplitting/index.jsx @@ -15,9 +15,11 @@ export default function CodeSplitting() { For a better perspective, how cool and complex it is: Webpack 2+ documentation on code splitting refer ‌ - - this page - + { + + this page + + } {' '} of {' '} diff --git a/src/shared/components/examples/GigsFeed/index.jsx b/src/shared/components/examples/GigsFeed/index.jsx index be45c1f8e..b54fe8200 100644 --- a/src/shared/components/examples/GigsFeed/index.jsx +++ b/src/shared/components/examples/GigsFeed/index.jsx @@ -2,7 +2,7 @@ import GigsFeed from 'containers/Dashboard/GigsFeed'; import React from 'react'; import './style.scss'; -import { PrimaryButton } from 'topcoder-react-ui-kit'; +import { PrimaryButton } from 'topcoder-react-ui-kit/src/shared/components/buttons'; export default class GigsFeedExample extends React.Component { constructor() { diff --git a/src/shared/components/examples/ThriveArticlesFeed/index.jsx b/src/shared/components/examples/ThriveArticlesFeed/index.jsx index 2883bd293..c6b9739c1 100644 --- a/src/shared/components/examples/ThriveArticlesFeed/index.jsx +++ b/src/shared/components/examples/ThriveArticlesFeed/index.jsx @@ -2,7 +2,7 @@ import ThriveArticlesFeedContainer from 'containers/Dashboard/ThriveArticlesFeed import React from 'react'; import './style.scss'; -import { PrimaryButton } from 'topcoder-react-ui-kit'; +import { PrimaryButton } from 'topcoder-react-ui-kit/src/shared/components/buttons'; export default class ThriveArticlesFeedExample extends React.Component { constructor() { diff --git a/src/shared/components/tc-communities/communities/cognitive/Resources/index.jsx b/src/shared/components/tc-communities/communities/cognitive/Resources/index.jsx index fcb142ceb..7f1b6c5dd 100644 --- a/src/shared/components/tc-communities/communities/cognitive/Resources/index.jsx +++ b/src/shared/components/tc-communities/communities/cognitive/Resources/index.jsx @@ -121,14 +121,16 @@ export default function Resources({ > It’s easy! ‌ - - Click here - + { + + Click here + + } {' '} to get an IBM Cloud account. diff --git a/src/shared/components/tc-communities/communities/community-2/Learn/index.jsx b/src/shared/components/tc-communities/communities/community-2/Learn/index.jsx index 8e57166ba..fbedfdf65 100644 --- a/src/shared/components/tc-communities/communities/community-2/Learn/index.jsx +++ b/src/shared/components/tc-communities/communities/community-2/Learn/index.jsx @@ -49,7 +49,7 @@ export default function Learn() { Meticulously crafted comprehensive learning paths, detailed study material, engaging case studies, training projects to systematically enhance your career, development environments to practice, convenient online accessibility, opportunity to connect with mentors, peers, SMEs of various technologies – a variety of resources bringing people and technology together for an innovative and valuable learning experience.

- Our compelling learning environment across wide range of emerging technologies helps you in mastering today’s most essential skills, that brings your knowledge to the next level, step by step, which ultimately creates a more effective learning experience. + Our compelling learning environment across wide range of emerging technologies helps you in mastering today’s most essential skills, that brings your knowledge to the next level, step by step, which ultimately creates a more effective learning experience.

Are you ready to step onto the innovative journey of learning? diff --git a/src/shared/components/tc-communities/communities/iot/AssetDetail/ShareSocial.jsx b/src/shared/components/tc-communities/communities/iot/AssetDetail/ShareSocial.jsx index 17ddb840b..d78929121 100644 --- a/src/shared/components/tc-communities/communities/iot/AssetDetail/ShareSocial.jsx +++ b/src/shared/components/tc-communities/communities/iot/AssetDetail/ShareSocial.jsx @@ -1,5 +1,7 @@ /* global document window */ +/* eslint-disable jsx-a11y/href-no-hash */ + import React from 'react'; import TwitterIcon from '../../../../../../assets/images/social/icon_twitter.svg'; diff --git a/src/shared/containers/ContentfulLoader.jsx b/src/shared/containers/ContentfulLoader.jsx index a577cbe41..858642d33 100644 --- a/src/shared/containers/ContentfulLoader.jsx +++ b/src/shared/containers/ContentfulLoader.jsx @@ -437,14 +437,7 @@ function mapStateToProps(state, ownProps) { return ownProps.preview ? st.preview : st.published; } -/** - * Creates dispatch callbacks used by ContentfulLoader. Async callbacks return - * the Redux middleware promise so SSR can observe dispatch failures. - * @param {Function} dispatch Redux store dispatch function. - * @return {Object} ContentfulLoader dispatch callbacks. - * @throws {Error} Propagates synchronous errors raised by dispatch. - */ -export function mapDispatchToProps(dispatch) { +function mapDispatchToProps(dispatch) { const a = actions.contentful; const bC = a.bookContent; const bQ = a.bookQuery; @@ -459,16 +452,16 @@ export function mapDispatchToProps(dispatch) { const uuid = shortId(); dispatch(a.getContentInit(uuid, contentId, target, preview, spaceName, environment)); const action = a.getContentDone(uuid, contentId, target, preview, spaceName, environment); - /* Return redux-promise's chain so SSR observes dispatch failures. */ - return dispatch(action); + dispatch(action); + return action.payload; }, queryContent: (queryId, query, target, preview, spaceName, environment) => { const uuid = shortId(); const q = _.isObject(query) ? query : null; dispatch(a.queryContentInit(uuid, queryId, target, preview, spaceName, environment)); const action = a.queryContentDone(uuid, queryId, target, q, preview, spaceName, environment); - /* Return redux-promise's chain so SSR observes dispatch failures. */ - return dispatch(action); + dispatch(action); + return action.payload; }, }; } diff --git a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx index ee0e4d9bb..dd75721b0 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx @@ -10,7 +10,7 @@ import PT from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; import { isValidEmail } from 'utils/tc'; -import withOptimizely from 'utils/withOptimizely'; +import { withOptimizely } from '@optimizely/react-sdk'; const cookies = require('browser-cookies'); diff --git a/src/shared/containers/Gigs/RecruitCRMJobs.jsx b/src/shared/containers/Gigs/RecruitCRMJobs.jsx index d40436c76..6d146e133 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobs.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobs.jsx @@ -16,7 +16,7 @@ import { getSalaryType, getCustomField } from 'utils/gigs'; import IconBlackLocation from 'assets/images/icon-black-location.svg'; import { config, Link, isomorphy } from 'topcoder-react-utils'; import { getQuery, updateQuery } from 'utils/url'; -import withOptimizely from 'utils/withOptimizely'; +import { withOptimizely } from '@optimizely/react-sdk'; import GigHeader from 'components/Gigs/GigHeader'; import './jobLisingStyles.scss'; diff --git a/src/shared/containers/Gigs/_RecruitCRMJobs_ab-v1.jsx b/src/shared/containers/Gigs/_RecruitCRMJobs_ab-v1.jsx index 5e59b1eef..9c5f9bfa7 100644 --- a/src/shared/containers/Gigs/_RecruitCRMJobs_ab-v1.jsx +++ b/src/shared/containers/Gigs/_RecruitCRMJobs_ab-v1.jsx @@ -16,7 +16,7 @@ import { getSalaryType, getCustomField } from 'utils/gigs'; import IconBlackLocation from 'assets/images/icon-black-location.svg'; import { config, Link, isomorphy } from 'topcoder-react-utils'; import { getQuery, updateQuery } from 'utils/url'; -import withOptimizely from 'utils/withOptimizely'; +import { withOptimizely } from '@optimizely/react-sdk'; import './jobLisingStyles.scss'; const cookies = require('browser-cookies'); diff --git a/src/shared/containers/TopcoderHeader/index.jsx b/src/shared/containers/TopcoderHeader/index.jsx index 9588c70f2..ce478cb4f 100644 --- a/src/shared/containers/TopcoderHeader/index.jsx +++ b/src/shared/containers/TopcoderHeader/index.jsx @@ -60,7 +60,6 @@ const TopcoderHeader = ({ auth, location }) => { return (

ids.add(item.id)); +// const ids = new Set(); +// loaded.forEach(item => ids.add(item.id)); - // /* Fetching 0 page of past challenges also drops any past challenges - // * loaded to the state before. */ - // // const filter = state.lastRequestedPageOfPastChallenges - // // ? item => !ids.has(item.id) - // // : item => !ids.has(item.id) && item.status !== 'COMPLETED' && item.status !== 'PAST'; +// /* Fetching 0 page of past challenges also drops any past challenges +// * loaded to the state before. */ +// // const filter = state.lastRequestedPageOfPastChallenges +// // ? item => !ids.has(item.id) +// // : item => !ids.has(item.id) && item.status !== 'COMPLETED' && item.status !== 'PAST'; - // const challenges = state.challenges.filter(filter).concat(loaded); +// const challenges = state.challenges.filter(filter).concat(loaded); - // // let keepPastPlaceholders = false; - // // if (loaded.length) { - // // const ff = Filter.getFilterFunction(frontFilter); - // keepPastPlaceholders = challenges.filter(ff).length - state.challenges.filter(ff).length < 10; - // // } +// // let keepPastPlaceholders = false; +// // if (loaded.length) { +// // const ff = Filter.getFilterFunction(frontFilter); +// keepPastPlaceholders = challenges.filter(ff).length - state.challenges.filter(ff).length < 10; +// // } - // // const pastSearchTimestamp = state.pastSearchTimestamp && state.pastSearchTimestamp > 0 - // // ? state.pastSearchTimestamp : Date.now(); +// // const pastSearchTimestamp = state.pastSearchTimestamp && state.pastSearchTimestamp > 0 +// // ? state.pastSearchTimestamp : Date.now(); // return { // ...state, diff --git a/src/shared/reducers/challenge-listing/sidebar.js b/src/shared/reducers/challenge-listing/sidebar.js index 84130c17e..bb3822811 100644 --- a/src/shared/reducers/challenge-listing/sidebar.js +++ b/src/shared/reducers/challenge-listing/sidebar.js @@ -2,6 +2,7 @@ * Challenge listing sidebar reducer. */ +/* global alert */ /* eslint-disable no-alert */ import _ from 'lodash'; diff --git a/src/shared/routes/TimelineWall/Router.jsx b/src/shared/routes/TimelineWall/Router.jsx index b4a1e377b..a66c52c94 100644 --- a/src/shared/routes/TimelineWall/Router.jsx +++ b/src/shared/routes/TimelineWall/Router.jsx @@ -1,3 +1,5 @@ +/* global document window */ + import React from 'react'; import Footer from 'components/TopcoderFooter'; diff --git a/src/shared/routes/index.jsx b/src/shared/routes/index.jsx index 819f55203..86932d06c 100644 --- a/src/shared/routes/index.jsx +++ b/src/shared/routes/index.jsx @@ -33,7 +33,7 @@ import ProfileRedirect from './ProfileRedirect'; import RedirectMemberSearch from './RedirectMemberSearch'; import SettingRedirect from './Settings/SettingRedirect'; -import './styles.scss'; +import './Topcoder/styles.scss'; function Routes({ communityId }) { const metaTags = ( diff --git a/src/shared/routes/styles.scss b/src/shared/routes/styles.scss deleted file mode 100644 index 2c4232bd9..000000000 --- a/src/shared/routes/styles.scss +++ /dev/null @@ -1,17 +0,0 @@ -/* - * The root router owns a separate copy of the shared page layout so the - * asynchronous Topcoder route emits the stylesheet expected by AppChunk. - */ -.container { - display: flex; - min-height: 100vh; - flex-direction: column; - justify-content: space-between; - - > *:nth-child(2):not(:last-child) { - display: flex; - flex: 1 0 auto; - position: relative; - z-index: 1; - } -} diff --git a/src/shared/services/money.js b/src/shared/services/money.js index c0d10ddca..996ec4634 100644 --- a/src/shared/services/money.js +++ b/src/shared/services/money.js @@ -36,20 +36,12 @@ async function updateCache() { fx.rates = cache.rates; } -/** - * Starts a cache refresh without blocking synchronous callers. - * - * Used during module initialization and by the synchronous public methods. - * Refresh failures are consumed so existing cached rates remain available - * instead of creating an unhandled Promise rejection. - * @return {void} - */ -function refreshCacheInBackground() { - updateCache().catch(_.noop); +try { + updateCache(); +} catch (error) { + // exchange-rates failed, reason: socket hang up } -refreshCacheInBackground(); - /** * Converts specified amount of money to another currency. * @param {Number} amount Amount of money to convert. @@ -69,16 +61,21 @@ export async function convert(amount, to, from = 'USD') { } /** - * Converts an amount synchronously using cached rates while triggering a - * non-blocking refresh when the cache is stale. Refresh failures are ignored - * and leave the existing cache in place. - * @param {Number} amount Amount of money to convert. - * @param {String} to Target currency (3-letter code such as USD or EUR). - * @param {String} from Original currency. Defaults to USD. - * @return {Number} Converted amount. + * Same as convert(..), but works syncroneously (using cached rates). + * This function still triggers refreshement of the cached rates if necessary, + * but it does not wait for the result, and just uses cached rates for the + * actual conversion. It is safe to use anyway + * @param {Number} amount + * @param {String} to + * @param {String} from + * @return {Number} */ export function convertNow(amount, to, from = 'USD') { - refreshCacheInBackground(); + try { + updateCache(); + } catch (error) { + // exchange-rates failed, reason: socket hang up + } return fx.convert(amount, { from, to }); } @@ -96,11 +93,14 @@ export async function getRates() { } /** - * Returns cached exchange rates synchronously while triggering a non-blocking - * refresh when the cache is stale. Refresh failures leave the cache unchanged. - * @return {Object} A clone of the cached exchange-rate data. + * Same as getRates(..) but works syncroneously, using the cached rates. + * @return {Promise} */ export function getRatesNow() { - refreshCacheInBackground(); + try { + updateCache(); + } catch (error) { + // exchange-rates failed, reason: socket hang up + } return _.cloneDeep(cache); } diff --git a/src/shared/utils/SSR.jsx b/src/shared/utils/SSR.jsx index 4417aaf00..f2e7808d6 100644 --- a/src/shared/utils/SSR.jsx +++ b/src/shared/utils/SSR.jsx @@ -47,8 +47,7 @@ export async function DoSSR(request, store, App) { * with the rendering of decorated component, using updated store for that. * @param {Function} updateStore Given Redux store and ExpressJS HTTP request, * as its two arguments, this function should update the store to the necessary - * state. It should return a promise that resolves when ready. Rejections stay - * on the promise collected by DoSSR for the renderer's normal error handling. + * state. It should return a promise that resolves when ready. */ export default function SSR(checkStore, updateStore) { return Component => (props) => { @@ -56,7 +55,11 @@ export default function SSR(checkStore, updateStore) { const Wrapper = withRouter(({ location, staticContext }) => { const { request, ssrPromises, store } = staticContext; if (checkStore(store, props, request)) return ; - const promise = updateStore(store, props, request).then(() => { + const promise = updateStore(store, props, request); + if (ssrPromises) { + ssrPromises.push(promise); + } + promise.then(() => { ReactDOM.renderToString(( @@ -65,11 +68,6 @@ export default function SSR(checkStore, updateStore) { )); }); - if (ssrPromises) { - ssrPromises.push(promise); - } else { - promise.catch(() => undefined); - } return null; }); return ; diff --git a/src/shared/utils/secureRandom.js b/src/shared/utils/secureRandom.js index 63a238f5a..190621fa6 100644 --- a/src/shared/utils/secureRandom.js +++ b/src/shared/utils/secureRandom.js @@ -7,7 +7,7 @@ const getCryptoLibrary = () => { return nodeCrypto; }; -export default function getSecureRandomIndex(min, max) { +export default function (min, max) { const crypto = getCryptoLibrary(); const random = new Uint32Array(1); if (typeof crypto.getRandomValues === 'function') { diff --git a/src/shared/utils/withOptimizely.jsx b/src/shared/utils/withOptimizely.jsx deleted file mode 100644 index 1ad6d20b9..000000000 --- a/src/shared/utils/withOptimizely.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { useOptimizelyClient } from '@optimizely/react-sdk'; - -/** - * Compatibility wrapper for class components that still consume the - * Optimizely client through an injected prop. - * - * @param {React.ComponentType} Component component receiving `optimizely` - * @returns {React.ComponentType} wrapped component - */ -export default function withOptimizely(Component) { - function WithOptimizely(props) { - const optimizely = useOptimizelyClient(); - - return ; - } - - WithOptimizely.displayName = `withOptimizely(${ - Component.displayName || Component.name || 'Component' - })`; - - return WithOptimizely; -} diff --git a/src/shared/utils/xml2json.js b/src/shared/utils/xml2json.js index c0a60ec6c..fceb89abf 100644 --- a/src/shared/utils/xml2json.js +++ b/src/shared/utils/xml2json.js @@ -6,27 +6,7 @@ import 'isomorphic-fetch'; import { config, isomorphy } from 'topcoder-react-utils'; -const XMLParser = isomorphy.isServerSide() - ? require('fast-xml-parser').XMLParser - : null; - -/** - * Matches the former xml2json package's representation of empty elements. - * - * @param {*} value parsed XML value. - * @return {*} value with empty elements represented as objects. - */ -function normalizeEmptyElements(value) { - if (value === '') return {}; - if (Array.isArray(value)) return value.map(normalizeEmptyElements); - if (value && typeof value === 'object') { - return Object.keys(value).reduce((result, key) => ({ - ...result, - [key]: normalizeEmptyElements(value[key]), - }), {}); - } - return value; -} +const xml2json = isomorphy.isServerSide() ? require('xml2json') : null; /** * Makes XML -> JSON conversion. @@ -34,13 +14,7 @@ function normalizeEmptyElements(value) { * @return {Promise} Resolves to JSON document. */ export function toJson(xml) { - if (XMLParser) { - const parser = new XMLParser({ - attributeNamePrefix: '', - ignoreAttributes: false, - }); - return Promise.resolve(normalizeEmptyElements(parser.parse(xml))); - } + if (xml2json) return Promise.resolve(xml2json.toJson(xml, { object: true })); return fetch('/community-app-assets/api/xml2json', { body: JSON.stringify({ xml }), headers: { diff --git a/src/test/jmeter/Community-25UV.jmx b/src/test/jmeter/Community-25UV.jmx index c381f460e..b86712680 100644 --- a/src/test/jmeter/Community-25UV.jmx +++ b/src/test/jmeter/Community-25UV.jmx @@ -53,14 +53,14 @@ false - ${__P(TC_M2M_CLIENT_ID,)} + jGIf2pd3f44B1jqvOai30BIKTZanYBfU = true client_id false - ${__P(TC_M2M_CLIENT_SECRET,)} + ldzqVaVEbqhwjM5KtZ79sG8djZpAVK8Z7qieVcC3vRjI4NirgcinKSBpPwk6mYYP = true client_secret diff --git a/vendor/glob-compat/README.md b/vendor/glob-compat/README.md deleted file mode 100644 index dabd09a3e..000000000 --- a/vendor/glob-compat/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# glob CommonJS compatibility adapter - -Some legacy Community App build plugins expect `require('glob')` to return a -callable object, while maintained glob versions expose functions as named -exports. - -This adapter preserves the callable CommonJS shape, including legacy callback -handling, and forwards named exports to `glob@13.0.6`. It contains no matching -implementation of its own. - -Browser bundlers resolve `browser.js`, a fail-fast facade with the same callable -shape. This keeps server-only transitive imports from bundling glob's Node.js -implementation or core-module dependencies. diff --git a/vendor/glob-compat/browser.js b/vendor/glob-compat/browser.js deleted file mode 100644 index c0e13a07a..000000000 --- a/vendor/glob-compat/browser.js +++ /dev/null @@ -1,37 +0,0 @@ -const MESSAGE = 'glob is only available in Node.js'; - -function unavailable() { - throw new Error(MESSAGE); -} - -/** - * Keeps server-only dependencies importable in a browser bundle without - * pulling glob's Node.js implementation and core-module dependencies into it. - * - * @param {String|String[]} pattern Unused glob pattern. - * @param {Object|Function} options Unused options or legacy callback. - * @param {Function} callback Optional legacy callback. - * @return {Promise|undefined} Rejected promise, or callback result. - */ -function glob(pattern, options, callback) { - const done = typeof options === 'function' ? options : callback; - const error = new Error(MESSAGE); - - if (done) { - done(error); - return undefined; - } - - return Promise.reject(error); -} - -glob.glob = glob; -glob.globSync = unavailable; -glob.sync = unavailable; -glob.hasMagic = unavailable; -glob.escape = unavailable; -glob.unescape = unavailable; -glob.Glob = unavailable; -glob.Ignore = unavailable; - -module.exports = glob; diff --git a/vendor/glob-compat/index.js b/vendor/glob-compat/index.js deleted file mode 100644 index 4b8e1ff66..000000000 --- a/vendor/glob-compat/index.js +++ /dev/null @@ -1,43 +0,0 @@ -const modern = require('glob-modern'); - -/** - * Preserves the callable CommonJS API used by legacy build plugins while - * delegating matching behavior to the maintained glob release. - * - * @param {String|String[]} pattern Glob pattern or patterns. - * @param {Object|Function} options Glob options or a legacy callback. - * @param {Function} callback Optional legacy callback. - * @return {Promise} Promise resolving to matching paths. - */ -function glob(pattern, options, callback) { - let globOptions = options; - let done = callback; - - if (typeof options === 'function') { - globOptions = undefined; - done = options; - } - - const result = modern.glob(pattern, globOptions); - - if (done) { - result.then( - matches => done(null, matches), - error => done(error), - ); - } - - return result; -} - -Object.assign(glob, modern); - -module.exports = glob; -// Explicit assignments preserve named-import detection for ESM consumers. -module.exports.glob = modern.glob; -module.exports.globSync = modern.globSync; -module.exports.hasMagic = modern.hasMagic; -module.exports.escape = modern.escape; -module.exports.unescape = modern.unescape; -module.exports.Glob = modern.Glob; -module.exports.Ignore = modern.Ignore; diff --git a/vendor/glob-compat/package.json b/vendor/glob-compat/package.json deleted file mode 100644 index 4fdbd7d8c..000000000 --- a/vendor/glob-compat/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "glob", - "version": "13.0.6", - "description": "CommonJS compatibility adapter for maintained glob", - "main": "index.js", - "browser": "browser.js", - "private": true, - "dependencies": { - "glob-modern": "npm:glob@13.0.6" - }, - "engines": { - "node": ">=20" - } -} diff --git a/vendor/minimatch-compat/README.md b/vendor/minimatch-compat/README.md deleted file mode 100644 index d63a3077f..000000000 --- a/vendor/minimatch-compat/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# minimatch CommonJS compatibility adapter - -Some legacy Community App build plugins still call `require('minimatch')` as a -function. Maintained minimatch versions expose that function as the named -`minimatch` export. - -This adapter preserves the old callable shape and forwards every operation and -named export to `minimatch@10.2.6`. It contains no matching implementation of -its own. diff --git a/vendor/minimatch-compat/index.js b/vendor/minimatch-compat/index.js deleted file mode 100644 index 7632fd2ac..000000000 --- a/vendor/minimatch-compat/index.js +++ /dev/null @@ -1,21 +0,0 @@ -const modern = require('minimatch-modern'); - -const match = modern.minimatch; - -/** - * Preserves the callable CommonJS API used by legacy build plugins while - * delegating all matching behavior to the maintained minimatch release. - * - * @param {String} value Path or value to test. - * @param {String} pattern Glob pattern. - * @param {Object} options Minimatch options. - * @return {Boolean} Whether the value matches the pattern. - */ -function minimatch(value, pattern, options) { - return match(value, pattern, options); -} - -Object.assign(minimatch, modern); -minimatch.minimatch = minimatch; - -module.exports = minimatch; diff --git a/vendor/minimatch-compat/package.json b/vendor/minimatch-compat/package.json deleted file mode 100644 index a9617bf3b..000000000 --- a/vendor/minimatch-compat/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "minimatch", - "version": "10.2.6", - "description": "Callable CommonJS compatibility adapter for maintained minimatch", - "main": "index.js", - "private": true, - "dependencies": { - "minimatch-modern": "npm:minimatch@10.2.6" - }, - "engines": { - "node": ">=20" - } -} diff --git a/vendor/tc-auth-lib-compat/README.md b/vendor/tc-auth-lib-compat/README.md deleted file mode 100644 index 380a5225d..000000000 --- a/vendor/tc-auth-lib-compat/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Topcoder authentication library compatibility adapter - -The upstream `@topcoder-platform/tc-auth-lib@1.0.2` source mixes CommonJS -`require()` calls with ESM `export` declarations in the same files. Node 24 -detects those files as ESM and rejects the CommonJS calls before the Community -App server can start. - -This package preserves the public API using consistent CommonJS modules. It -also restricts connector messages to the configured iframe and its exact -origin. The adapter can be removed once the upstream package publishes an -equivalent Node-compatible release. diff --git a/vendor/tc-auth-lib-compat/index.js b/vendor/tc-auth-lib-compat/index.js deleted file mode 100644 index cea0245c8..000000000 --- a/vendor/tc-auth-lib-compat/index.js +++ /dev/null @@ -1,15 +0,0 @@ -const { - configureConnector, - getFreshToken, -} = require('./src/connector-wrapper'); -const { - decodeToken, - isTokenExpired, -} = require('./src/token'); - -module.exports = { - configureConnector, - decodeToken, - getFreshToken, - isTokenExpired, -}; diff --git a/vendor/tc-auth-lib-compat/package.json b/vendor/tc-auth-lib-compat/package.json deleted file mode 100644 index 4b51c1e6a..000000000 --- a/vendor/tc-auth-lib-compat/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "@topcoder-platform/tc-auth-lib", - "version": "1.0.2-community.1", - "description": "Node-compatible Topcoder authentication library adapter", - "main": "index.js", - "private": true, - "dependencies": { - "lodash": "^4.18.1" - }, - "engines": { - "node": ">=20" - } -} diff --git a/vendor/tc-auth-lib-compat/src/connector-wrapper.js b/vendor/tc-auth-lib-compat/src/connector-wrapper.js deleted file mode 100644 index 90e89b3ad..000000000 --- a/vendor/tc-auth-lib-compat/src/connector-wrapper.js +++ /dev/null @@ -1,130 +0,0 @@ -/* global window */ - -const { createFrame } = require('./iframe'); -const { getToken, isTokenExpired } = require('./token'); - -let iframe = null; -let loading = null; -let connectorOrigin = ''; -let mock = false; -let token = ''; - -/** - * Configures the accounts authentication iframe. - * - * @param {Object} options Connector configuration. - * @return {void} - */ -function configureConnector({ - connectorUrl, - frameId, - mockMode, - mockToken, -}) { - if (mockMode) { - mock = true; - token = mockToken; - return; - } - - if (iframe) { - // eslint-disable-next-line no-console - console.warn( - 'tc-accounts connector can only be configured once; this request was ignored.', - ); - return; - } - - connectorOrigin = new URL(connectorUrl, window.location.href).origin; - iframe = createFrame(frameId, connectorUrl); - loading = new Promise((resolve) => { - iframe.onload = () => { - loading = null; - resolve(); - }; - }); -} - -function requestToken() { - const currentToken = getToken('tcjwt'); - - if (currentToken && !isTokenExpired(currentToken, 65)) { - return Promise.resolve({ token: currentToken }); - } - - return new Promise((resolve, reject) => { - function receiveMessage(event) { - const data = event.data || {}; - const validSource = event.source === iframe.contentWindow; - const validOrigin = event.origin === connectorOrigin; - const validType = data.type === 'SUCCESS' || data.type === 'FAILURE'; - - if (!validSource || !validOrigin || !validType) { - return; - } - - window.removeEventListener('message', receiveMessage); - - if (data.type === 'SUCCESS') { - const refreshedToken = getToken('tcjwt'); - - if (refreshedToken) { - resolve({ token: refreshedToken }); - } else { - reject(new Error('tcjwt cookie not found')); - } - } else { - reject(new Error('Unable to refresh token')); - } - } - - window.addEventListener('message', receiveMessage); - iframe.contentWindow.postMessage( - { type: 'REFRESH_TOKEN' }, - connectorOrigin, - ); - }); -} - -function proxyCall() { - if (mock) { - throw new Error( - 'Connector is in mock mode; proxyCall must not be invoked.', - ); - } - - if (!iframe) { - throw new Error('Connector has not been configured'); - } - - if (loading) { - loading = loading.then(requestToken); - return loading; - } - - return requestToken(); -} - -/** - * Gets a fresh authentication token. - * - * @return {Promise} Refreshed token. - */ -function getFreshToken() { - if (mock) { - if (token) { - return Promise.resolve(token); - } - - return Promise.reject(new Error( - 'Connector is in mock mode, but no token was specified.', - )); - } - - return proxyCall().then(data => data.token); -} - -module.exports = { - configureConnector, - getFreshToken, -}; diff --git a/vendor/tc-auth-lib-compat/src/iframe.js b/vendor/tc-auth-lib-compat/src/iframe.js deleted file mode 100644 index 6dfdbde3a..000000000 --- a/vendor/tc-auth-lib-compat/src/iframe.js +++ /dev/null @@ -1,24 +0,0 @@ -/* global document */ - -/** - * Creates the hidden iframe used by the accounts authentication connector. - * - * @param {String} id Iframe element ID. - * @param {String} src Connector URL. - * @return {HTMLIFrameElement} Configured iframe. - */ -function createFrame(id, src) { - const iframe = document.createElement('iframe'); - - iframe.id = id; - iframe.src = src; - iframe.width = 0; - iframe.height = 0; - iframe.setAttribute('frameborder', '0'); - - document.body.appendChild(iframe); - - return iframe; -} - -module.exports = { createFrame }; diff --git a/vendor/tc-auth-lib-compat/src/token.js b/vendor/tc-auth-lib-compat/src/token.js deleted file mode 100644 index c229116f7..000000000 --- a/vendor/tc-auth-lib-compat/src/token.js +++ /dev/null @@ -1,121 +0,0 @@ -/* global atob document */ - -const _ = require('lodash'); - -function parseCookie(cookie) { - return _.fromPairs(cookie - .split(';') - .map(pair => pair.split('=').map(part => part.trim()))); -} - -function readCookie(name) { - return parseCookie(document.cookie)[name]; -} - -/** - * Gets a browser cookie by name. - * - * @param {String} key Cookie name. - * @return {String|undefined} Cookie value. - */ -function getToken(key) { - return readCookie(key); -} - -function decodeBase64Url(value) { - let encoded = value.replace(/-/g, '+').replace(/_/g, '/'); - - switch (encoded.length % 4) { - case 0: - break; - case 2: - encoded += '=='; - break; - case 3: - encoded += '='; - break; - default: - throw new Error('Illegal base64url string'); - } - - const binary = atob(encoded); - const escaped = Array.from( - binary, - character => `%${character.charCodeAt(0).toString(16).padStart(2, '0')}`, - ).join(''); - - return decodeURIComponent(escaped); -} - -/** - * Decodes a JWT payload without attempting signature validation. - * - * @param {String} token Encoded JWT. - * @return {Object} Decoded JWT payload with canonical Topcoder claims. - */ -function decodeToken(token) { - const parts = token.split('.'); - - if (parts.length !== 3) { - throw new Error('The token is invalid'); - } - - const decoded = decodeBase64Url(parts[1]); - - if (!decoded) { - throw new Error('Cannot decode the token'); - } - - const payload = JSON.parse(decoded); - - payload.userId = _.parseInt(_.find( - payload, - (value, key) => key.includes('userId'), - )); - payload.handle = _.find( - payload, - (value, key) => key.includes('handle'), - ); - payload.roles = _.find( - payload, - (value, key) => key.includes('roles'), - ); - - return payload; -} - -function getTokenExpirationDate(token) { - const decoded = decodeToken(token); - - if (typeof decoded.exp === 'undefined') { - return null; - } - - const date = new Date(0); - date.setUTCSeconds(decoded.exp); - return date; -} - -/** - * Determines whether a JWT is expired or inside the supplied expiry offset. - * - * @param {String} token Encoded JWT. - * @param {Number} offsetSeconds Expiry offset in seconds. - * @return {Boolean} Whether the JWT is expired. - */ -function isTokenExpired(token, offsetSeconds = 0) { - const expiration = getTokenExpirationDate(token); - - if (expiration === null) { - return false; - } - - return expiration.valueOf() - <= (new Date().valueOf() + (offsetSeconds * 1000)); -} - -module.exports = { - decodeToken, - getToken, - isTokenExpired, -}; diff --git a/webpack.config.js b/webpack.config.js index 1d850051a..1a48063bc 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -6,12 +6,10 @@ /* eslint-disable global-require */ /* eslint-disable import/no-dynamic-require */ -module.exports = function buildConfig(env = {}) { - const mode = typeof env === 'string' ? env : env.mode; - const supportedModes = ['development', 'production', 'qa']; - if (!supportedModes.includes(mode)) { - throw new Error(`Unsupported Webpack environment: ${mode || '(missing)'}`); - } - // eslint-disable-next-line global-require, import/no-dynamic-require - return require(`./config/webpack/${mode}.js`); +module.exports = function buildConfig(env) { + // eslint-disable-next-line no-console + console.log('Building config for environment:', env); + const config = require(`./config/webpack/${env}.js`); + console.log('config', JSON.stringify(config)); // eslint-disable-line no-console + return config; };