diff --git a/src/components/my-career/CategoryCard.jsx b/src/components/my-career/CategoryCard.jsx index a35d85fe7..1d71f69b2 100644 --- a/src/components/my-career/CategoryCard.jsx +++ b/src/components/my-career/CategoryCard.jsx @@ -1,5 +1,5 @@ import React, { - useMemo, useState, useEffect, useContext, + useState, useEffect, useContext, } from 'react'; import PropTypes from 'prop-types'; @@ -9,10 +9,10 @@ import { useToggle, } from '@edx/paragon'; import { getConfig } from '@edx/frontend-platform/config'; -import algoliasearch from 'algoliasearch/lite'; import { AppContext } from '@edx/frontend-platform/react'; import LevelBars from './LevelBars'; import SkillsRecommendationCourses from './SkillsRecommendationCourses'; +import { useAlgoliaSearch } from '../../utils/hooks'; const CategoryCard = ({ topCategory }) => { const { skillsSubcategories } = topCategory; @@ -26,16 +26,7 @@ const CategoryCard = ({ topCategory }) => { const config = getConfig(); const { enterpriseConfig } = useContext(AppContext); - const courseIndex = useMemo( - () => { - const client = algoliasearch( - config.ALGOLIA_APP_ID, - config.ALGOLIA_SEARCH_API_KEY, - ); - return client.initIndex(config.ALGOLIA_INDEX_NAME); - }, - [config.ALGOLIA_APP_ID, config.ALGOLIA_INDEX_NAME, config.ALGOLIA_SEARCH_API_KEY], - ); + const [, courseIndex] = useAlgoliaSearch(config, config.ALGOLIA_INDEX_NAME); const filterRenderableSkills = (skills) => { const renderableSkills = []; diff --git a/src/components/search/Search.jsx b/src/components/search/Search.jsx index 2aaad3ea1..dbbb832ca 100644 --- a/src/components/search/Search.jsx +++ b/src/components/search/Search.jsx @@ -1,5 +1,5 @@ import React, { - useContext, useMemo, useEffect, + useContext, useEffect, } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { Helmet } from 'react-helmet'; @@ -9,7 +9,6 @@ import { getConfig } from '@edx/frontend-platform/config'; import { SearchHeader, SearchContext } from '@edx/frontend-enterprise-catalog-search'; import { useToggle, Stack } from '@edx/paragon'; -import algoliasearch from 'algoliasearch/lite'; import { useDefaultSearchFilters, useSearchCatalogs } from './data/hooks'; import { NUM_RESULTS_PER_PAGE, @@ -36,6 +35,7 @@ import SearchPathwayCard from '../pathway/SearchPathwayCard'; import { SubsidyRequestsContext } from '../enterprise-subsidy-requests'; import PathwayModal from '../pathway/PathwayModal'; import { useEnterpriseCuration } from './content-highlights/data'; +import { useAlgoliaSearch } from '../../utils/hooks'; const Search = () => { const { pathwayUUID } = useParams(); @@ -78,17 +78,7 @@ const Search = () => { }, [openLearnerPathwayModal, pathwayUUID]); const config = getConfig(); - const courseIndex = useMemo( - () => { - const client = algoliasearch( - config.ALGOLIA_APP_ID, - config.ALGOLIA_SEARCH_API_KEY, - ); - const cIndex = client.initIndex(config.ALGOLIA_INDEX_NAME); - return cIndex; - }, - [config.ALGOLIA_APP_ID, config.ALGOLIA_INDEX_NAME, config.ALGOLIA_SEARCH_API_KEY], - ); + const [, courseIndex] = useAlgoliaSearch(config, config.ALGOLIA_INDEX_NAME); const PAGE_TITLE = `${HEADER_TITLE} - ${enterpriseConfig.name}`; const shouldDisplayBalanceAlert = hasNoEnterpriseOffersBalance || hasLowEnterpriseOffersBalance; diff --git a/src/components/skills-quiz/SkillsQuizStepper.jsx b/src/components/skills-quiz/SkillsQuizStepper.jsx index f7131c344..75b2e5853 100644 --- a/src/components/skills-quiz/SkillsQuizStepper.jsx +++ b/src/components/skills-quiz/SkillsQuizStepper.jsx @@ -1,9 +1,8 @@ /* eslint-disable object-curly-newline */ -import React, { useEffect, useState, useContext, useMemo } from 'react'; +import React, { useEffect, useState, useContext } from 'react'; import { Button, Stepper, ModalDialog, Container, Form, Stack, } from '@edx/paragon'; -import algoliasearch from 'algoliasearch/lite'; import { Configure, InstantSearch } from 'react-instantsearch-dom'; import { getConfig } from '@edx/frontend-platform/config'; import { SearchContext } from '@edx/frontend-enterprise-catalog-search'; @@ -38,6 +37,7 @@ import { import { SkillsContext } from './SkillsContextProvider'; import { SET_KEY_VALUE } from './data/constants'; import { checkValidGoalAndJobSelected } from '../utils/skills-quiz'; +import { useAlgoliaSearch } from '../../utils/hooks'; import TopSkillsOverview from './TopSkillsOverview'; import SkillsQuizHeader from './SkillsQuizHeader'; @@ -48,18 +48,9 @@ import { fetchCourseEnrollments } from './data/service'; const SkillsQuizStepper = () => { const config = getConfig(); const { userId } = getAuthenticatedUser(); - const [searchClient, courseIndex, jobIndex] = useMemo( - () => { - const client = algoliasearch( - config.ALGOLIA_APP_ID, - config.ALGOLIA_SEARCH_API_KEY, - ); - const cIndex = client.initIndex(config.ALGOLIA_INDEX_NAME); - const jIndex = client.initIndex(config.ALGOLIA_INDEX_NAME_JOBS); - return [client, cIndex, jIndex]; - }, - [config.ALGOLIA_APP_ID, config.ALGOLIA_INDEX_NAME, config.ALGOLIA_INDEX_NAME_JOBS, config.ALGOLIA_SEARCH_API_KEY], - ); + const [, courseIndex] = useAlgoliaSearch(config, config.ALGOLIA_INDEX_NAME); + const [jobSearchClient, jobIndex] = useAlgoliaSearch(config, config.ALGOLIA_INDEX_NAME_JOBS); + const [currentStep, setCurrentStep] = useState(STEP1); const [isStudentChecked, setIsStudentChecked] = useState(false); const handleIsStudentCheckedChange = e => setIsStudentChecked(e.target.checked); @@ -198,7 +189,7 @@ const SkillsQuizStepper = () => {
({ ...jest.requireActual('@edx/frontend-platform/auth'), getAuthenticatedUser: () => ({ username: 'myspace-tom' }), + getAuthenticatedHttpClient: jest.fn(), })); jest.mock('@edx/frontend-enterprise-utils', () => ({ diff --git a/src/index.jsx b/src/index.jsx index 612e704e7..39dc6678a 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -34,6 +34,8 @@ initialize({ LICENSE_MANAGER_URL: process.env.LICENSE_MANAGER_URL || null, ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || null, ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || null, + ALGOLIA_SECURED_KEY_ENDPOINT: process.env.ALGOLIA_SECURED_KEY_ENDPOINT + || `${process.env.LMS_BASE_URL}/enterprise/api/v1/enterprise-customer/algolia_key/`, ALGOLIA_INDEX_NAME: process.env.ALGOLIA_INDEX_NAME || null, ALGOLIA_INDEX_NAME_JOBS: process.env.ALGOLIA_INDEX_NAME_JOBS || null, INTEGRATION_WARNING_DISMISSED_COOKIE_NAME: process.env.INTEGRATION_WARNING_DISMISSED_COOKIE_NAME || null, diff --git a/src/utils/common.js b/src/utils/common.js index 98646c725..ae53bb3f7 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -1,6 +1,7 @@ import Cookies from 'universal-cookie'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getConfig } from '@edx/frontend-platform/config'; +import { logError } from '@edx/frontend-platform/logging'; import dayjs from './dayjs'; export const isCourseEnded = endDate => dayjs(endDate) < dayjs(); @@ -66,6 +67,22 @@ export const loginRefresh = async () => { } }; +export const fetchAlgoliaSecuredApiKey = async () => { + const config = getConfig(); + const httpClient = getAuthenticatedHttpClient(); + + try { + const response = await httpClient.get(config.ALGOLIA_SECURED_KEY_ENDPOINT); + if (response && response.data) { + return response.data.key; + } + throw new Error('Response does not contain data'); + } catch (error) { + logError(error); + return null; + } +}; + export const fixedEncodeURIComponent = (str) => encodeURIComponent(str).replace(/[!()*]/g, (c) => `%${ c.charCodeAt(0).toString(16)}`); export const formatStringAsNumber = (str, radix = 10) => { diff --git a/src/utils/hooks.jsx b/src/utils/hooks.jsx index 33b4d1089..bd55096b4 100644 --- a/src/utils/hooks.jsx +++ b/src/utils/hooks.jsx @@ -1,6 +1,13 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import algoliasearch from 'algoliasearch'; +import { fetchAlgoliaSecuredApiKey } from './common'; + export const useRenderContactHelpText = (enterpriseConfig) => { const renderContactHelpText = useCallback( (LinkComponent = 'a') => { @@ -22,17 +29,41 @@ export const useRenderContactHelpText = (enterpriseConfig) => { return renderContactHelpText; }; +let cachedApiKey = null; + +export const useAlgoliaSearchApiKey = (config) => { + // If the search API key is not provided in the config, + // fetch it from `ALGOLIA_SECURED_KEY_ENDPOINT`. + + const [searchApiKey, setSearchApiKey] = useState(cachedApiKey || config.ALGOLIA_SEARCH_API_KEY); + + useEffect(() => { + const fetchApiKey = async () => { + const key = await fetchAlgoliaSecuredApiKey(); + cachedApiKey = key; + setSearchApiKey(key); + }; + + if (!searchApiKey) { + fetchApiKey(); + } + }, [searchApiKey]); + + return searchApiKey; +}; + export const useAlgoliaSearch = (config, indexName) => { + const algoliaSearchApiKey = useAlgoliaSearchApiKey(config); const [searchClient, searchIndex] = useMemo( () => { const client = algoliasearch( config.ALGOLIA_APP_ID, - config.ALGOLIA_SEARCH_API_KEY, + algoliaSearchApiKey, ); const index = client.initIndex(indexName || config.ALGOLIA_INDEX_NAME); return [client, index]; }, - [config.ALGOLIA_APP_ID, config.ALGOLIA_INDEX_NAME, config.ALGOLIA_SEARCH_API_KEY, indexName], + [config.ALGOLIA_APP_ID, config.ALGOLIA_INDEX_NAME, algoliaSearchApiKey, indexName], ); return [searchClient, searchIndex]; };