From 9676baf06516cc9897069c923007e3274eb35422 Mon Sep 17 00:00:00 2001 From: Nathan Franklin Date: Thu, 18 Jan 2024 13:37:54 -0600 Subject: [PATCH] task/WG-196: handle various environments (#189) * Bump node for react project's github actions * Add Dockerfile for react image * Fix Makefile * Fix typo * Handle various bae paths * Add hook to handle various environments * Remove kube info in README * Update README * Use configuration when building requet Refactor so that useAppConfiguration is no longer async * Update github action to account for needed secret_local * Improve jwt test for local dev * Improve comment as both hazmapper.local and localhost work equally * Fix prettier * Update login * Update ignore files for docker/git * Log to missing jwt as error * Remove double slashes from callback url * Add remove notes * Use Production GeoApi in example configuration * Memomize configuration hooks * Remove unused import * Fix import * Add react specific client ids * Fix base paths for staging/dev/prod * Add logging statements to debug dev * Add note about determining base paths * Set base URL dynamically * Add base * Fix stripping of double slashes * Fix base and add some debugger statments * Make public base path relative * Remove possibly unneded base url setting * Fix clientid configuration * Revert "Remove possibly unneded base url setting" This reverts commit 9751323558c450f25d4d7a06763df1ef7c4579e8. * Remove debugging and console statements * Add taggitUrl to configuration * Drop production as unused in react/vite * Fix linting and remove debugger statement * Add comment about WG-224 * Fix spellings and todo * Add backin useEffect to login page * Skip core-components when calculating test coverage * Add unit test * Rename deploy command to publish * Copy in secret_local.ts in docker image * Add comment about DS's dev(staging) --- .dockerignore | 9 +- .github/workflows/main.yml | 9 +- .gitignore | 24 +-- Makefile | 21 +- README.md | 44 +--- angular/src/app/services/env.service.ts | 2 +- react/.env.example | 9 - react/.gitignore | 1 + react/Dockerfile | 16 ++ react/index.html | 23 ++- react/jest.config.cjs | 6 +- react/package.json | 2 +- react/src/AppRouter.tsx | 5 +- .../__fixtures__/appConfigurationFixture.ts | 32 +++ react/src/__fixtures__/authStateFixtures.ts | 17 ++ react/src/hooks/environment/index.ts | 2 + .../hooks/environment/useAppConfiguration.ts | 190 ++++++++++++++++++ react/src/hooks/environment/useBasePath.ts | 28 +++ react/src/hooks/index.ts | 1 + react/src/hooks/projects/useProjects.ts | 1 - react/src/pages/Login/Login.tsx | 15 +- react/src/redux/api/geoapi.ts | 1 + react/src/redux/projectsSlice.ts | 1 + react/src/requests.test.ts | 47 +++++ react/src/requests.ts | 60 +++++- react/src/secret_local.example.ts | 9 + react/src/types/environment.ts | 95 +++++++++ react/src/types/index.ts | 1 + react/vite.config.ts | 90 +-------- 29 files changed, 583 insertions(+), 178 deletions(-) delete mode 100644 react/.env.example create mode 100644 react/Dockerfile create mode 100644 react/src/__fixtures__/appConfigurationFixture.ts create mode 100644 react/src/__fixtures__/authStateFixtures.ts create mode 100644 react/src/hooks/environment/index.ts create mode 100644 react/src/hooks/environment/useAppConfiguration.ts create mode 100644 react/src/hooks/environment/useBasePath.ts create mode 100644 react/src/requests.test.ts create mode 100644 react/src/secret_local.example.ts create mode 100644 react/src/types/environment.ts diff --git a/.dockerignore b/.dockerignore index 4d895b56..cea94d7d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,9 @@ -node_modules/ .git +**/node_modules +**/build + +# Angular jwt file +**/jwt.ts + +# React project secrets +**/secret_local.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 33e11662..02ecfc9a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -64,7 +64,7 @@ jobs: - name: Setup Node.js for use with actions uses: actions/setup-node@v1 with: - node-version: 16.x + node-version: 20.10.x - uses: actions/cache@v1 with: @@ -80,6 +80,7 @@ jobs: - name: Unit Tests run: | cd react + cp src/secret_local.example.ts src/secret_local.ts npm run test React-Linting: runs-on: ubuntu-latest @@ -88,7 +89,7 @@ jobs: - name: Setup Node.js for use with actions uses: actions/setup-node@v1 with: - node-version: 16.x + node-version: 20.10.x - uses: actions/cache@v1 with: @@ -112,7 +113,7 @@ jobs: - name: Setup Node.js for use with actions uses: actions/setup-node@v1 with: - node-version: 16.x + node-version: 20.10.x - uses: actions/cache@v1 with: @@ -128,5 +129,5 @@ jobs: - name: Build run: | cd react - cp .env.example .env + cp src/secret_local.example.ts src/secret_local.ts npm run build diff --git a/.gitignore b/.gitignore index 12b4a3cd..d9613dd6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,14 @@ -# See http://help.github.com/ignore-files/ for more about ignoring files. +**/node_modules +**/build +**/dist +**/tmp +**/out-tsc -# compiled output -/dist -src/environments/jwt.ts -/tmp -/out-tsc +# Angular jwt file +**/jwt.ts -angular/dist -angular/src/environments/jwt.ts -angular/tmp -angular/out-tsc -# Only exists if Bazel was run -/bazel-out -__pycache__ -# dependencies -node_modules +# React project secrets +**/secret_local.ts # profiling files chrome-profiler-events.json diff --git a/Makefile b/Makefile index 3ec5b55f..f7118e25 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,20 @@ TAG := $(shell git log --format=%h -1) -IMAGE ?= taccaci/hazmapper:$(TAG) + +.PHONY: build-angular +build-angular: + docker build -t taccaci/hazmapper:$(TAG) -f angular/Dockerfile . + docker tag taccaci/hazmapper:$(TAG) taccaci/hazmapper:latest + +.PHONY: build-react +build-react: + docker build -t taccaci/hazmapper-react:$(TAG) -f react/Dockerfile . + docker tag taccaci/hazmapper-react:$(TAG) taccaci/hazmapper-react:latest .PHONY: build build: - docker build -t $(IMAGE) -f angular/Dockerfile . - docker tag taccaci/hazmapper:${TAG} taccaci/hazmapper:latest + make build-angular && make build-react -.PHONY: deploy -deploy: - docker push $(IMAGE) +.PHONY: publish +publish: + docker push taccaci/hazmapper:$(TAG) + docker push taccaci/hazmapper-react:$(TAG) diff --git a/README.md b/README.md index a583a0e0..87faf3c7 100644 --- a/README.md +++ b/README.md @@ -13,40 +13,18 @@ See https://github.com/TACC-Cloud/geoapi which is an associated restful API. ## Local React Development (work-in-progress) -`react/` has the react client +`react/` has the React client - -#### Environments - -Environments are handled by vite via `vite.config.ts` and the `.env` files. - - -The `TARGET` specified in `.env` prefix determines which environment we are running/building in (this must be set inside the directory of each deployment). +To get started, create a local secret file for local development: ``` -TARGET="TARGET ENVIRONMENT" +cp react/src/secret_local.example.ts react/src/secret_local.ts ``` -Possible target environments are `development`, `staging`, and `production`. - -Then, for local development, the `GEOAPI_BACKEND` specified in `.env` will allow testing different backends. +Add the jwt retrieved from [Getting started](###getting-started) to `react/src/secret_local.ts`. +The `geoapiBackend` in ( see [react/src/secret_local.example.ts](react/src/secret_local.example.ts) ) can be used to select which backend `geoapi` is used by Hazmapper during local development (e.g. `EnvironmentType.Production`, `EnvironmentType.Staging`, `EnvironmentType.Dev`, * `EnvironmentType.Local` -First, to set the target backend for local development, edit the `.env` file and add the target backend. -Possible target backends are `development`, `staging`, and `production`. -``` -GEOAPI_BACKEND="TARGET BACKEND" -``` - - -Then add the jwt retrieved from [Getting started](###getting-started) to `.env`. - -``` -JWT="YOUR JWT FROM ABOVE" -``` - - -Furthermore, additional environments defined in `vite.config.ts` will be accessible in the react codebase via `process.env.*`. (e.g. `process.env.apiUrl`) - +See https://github.com/TACC-Cloud/geoapi for more details on running geoapi locally. #### Run @@ -55,11 +33,12 @@ npm ci npm run dev ``` +Navigate to `http://localhost:4200/` or `http://hazmapper.local:4200/`. (Note that `hazmapper.local` needs to be added to your `/etc/hosts`) + #### Running unit tests Run `npm run test` - #### Running linters Run `npm run lint` to run linter @@ -78,7 +57,7 @@ The app will automatically reload if you change any of the source files. ### Configuring which geoapi-backend is used -The `backend` in [src/environments/environment.ts](src/environments/environment.ts) can be used to select which backend `geoapi` is used by the app: +The `backend` in [angular/src/environments/environment.ts](angular/src/environments/environment.ts) can be used to select which backend `geoapi` is used by the app: * `EnvironmentType.Production` * `EnvironmentType.Staging` @@ -103,8 +82,3 @@ Run `npm run lint:css -- --fix` to fix css files. ### Code scaffolding Run `ng generate component components/component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. - -## Kubernetes (Production/Staging environments) - -Information on Kubernetes configuration for production and staging environments can be found in the [kube/README.md](kube/README.md) including information -on kube commands and Jenkins deployment workflows. diff --git a/angular/src/app/services/env.service.ts b/angular/src/app/services/env.service.ts index 1ae02ef2..88a0e6a4 100644 --- a/angular/src/app/services/env.service.ts +++ b/angular/src/app/services/env.service.ts @@ -155,7 +155,7 @@ export class EnvService { } else if (/^hazmapper.tacc.utexas.edu/.test(hostname) && pathname.startsWith('/dev')) { this._env = EnvironmentType.Dev; this._apiUrl = this.getApiUrl(this.env); - this._taggitUrl = origin + '/taggit-dev'; /* doesn't yet exist */ + this._taggitUrl = origin + '/taggit-dev'; this._portalUrl = this.getPortalUrl(this.env); this._clientId = 'oEuGsl7xi015wnrEpxIeUmvzc6Qa'; this._baseHref = '/dev/'; diff --git a/react/.env.example b/react/.env.example deleted file mode 100644 index e7a0a9f4..00000000 --- a/react/.env.example +++ /dev/null @@ -1,9 +0,0 @@ -# Copy this to .env -# TARGET can be production, staging, or development -TARGET=development -# GEOAPI_BACKEND can be production, staging, or development -GEOAPI_BACKEND=development -# Not in use yet -DESIGNSAFE_PORTAL= -# Replace with jwt in local environment (refer to README.md) -JWT= diff --git a/react/.gitignore b/react/.gitignore index 860f7c01..ed835a8e 100644 --- a/react/.gitignore +++ b/react/.gitignore @@ -25,3 +25,4 @@ dist-ssr # Environment .env +secret_local.ts diff --git a/react/Dockerfile b/react/Dockerfile new file mode 100644 index 00000000..a9798261 --- /dev/null +++ b/react/Dockerfile @@ -0,0 +1,16 @@ +FROM node:20.10.0-alpine3.18 as node + +RUN mkdir /www +COPY react /www +WORKDIR /www + +# Creating dummy local file as it is not dynamically loaded; not actually used in prod TODO_REACT +COPY react/src/secret_local.example.ts src/secret_local.ts + +RUN npm ci +RUN npm run build + +FROM nginx:1.24-alpine +WORKDIR / +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=node /www/dist/ /usr/share/nginx/html diff --git a/react/index.html b/react/index.html index bb734a44..bd61da09 100644 --- a/react/index.html +++ b/react/index.html @@ -5,7 +5,28 @@ Hazmapper - + +
diff --git a/react/jest.config.cjs b/react/jest.config.cjs index d26a0805..9e7d0d42 100644 --- a/react/jest.config.cjs +++ b/react/jest.config.cjs @@ -28,9 +28,9 @@ module.exports = { coverageDirectory: 'coverage', // An array of regexp pattern strings used to skip coverage collection - // coveragePathIgnorePatterns: [ - // "\\\\node_modules\\\\" - // ], + coveragePathIgnorePatterns: [ + "src/core-components/" + ], // Indicates which provider should be used to instrument code for coverage // coverageProvider: "babel", diff --git a/react/package.json b/react/package.json index 18079751..78a4efa3 100644 --- a/react/package.json +++ b/react/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "tsc && vite build --base=./", "preview": "vite preview", "test": "jest --testPathIgnorePatterns='/src/core-components/'", "lint": "npm run lint:js && npm run prettier:check", diff --git a/react/src/AppRouter.tsx b/react/src/AppRouter.tsx index 9fcc5eed..ca1a0c74 100644 --- a/react/src/AppRouter.tsx +++ b/react/src/AppRouter.tsx @@ -16,6 +16,7 @@ import Callback from './pages/Callback/Callback'; import StreetviewCallback from './pages/StreetviewCallback/StreetviewCallback'; import { RootState } from './redux/store'; import { isTokenValid } from './utils/authUtils'; +import { useBasePath } from './hooks/environment'; interface ProtectedRouteProps { isAuthenticated: boolean; @@ -41,8 +42,10 @@ function AppRouter() { isTokenValid(state.auth.token) ); + const basePath = useBasePath(); + return ( - + { + const basePath = useBasePath(); + + const appConfiguration = useMemo(() => { + const hostname = window && window.location && window.location.hostname; + const pathname = window && window.location && window.location.pathname; + + const mapillaryConfig: MapillaryConfiguration = { + authUrl: 'https://www.mapillary.com/connect', + tokenUrl: 'https://graph.mapillary.com/token', + apiUrl: 'https://graph.mapillary.com/', + tileUrl: 'https://tiles.mapillary.com/', + scope: + 'user:email+user:read+user:write+public:write+public:upload+private:read+private:write+private:upload', + clientSecret: '', + clientId: '', + clientToken: '', + }; + + if (/^localhost/.test(hostname) || /^hazmapper.local/.test(hostname)) { + // Check if jwt has been set properly if we are using local geoapi + if ( + localDevelopmentConfiguration.geoapiBackend === + GeoapiBackendEnvironment.Local + ) { + if ( + localDevelopmentConfiguration.jwt.startsWith('INSERT YOUR JWT HERE') + ) { + console.error( + 'JWT has not been added to secret_local.ts; see README' + ); + throw new Error('JWT has not been added to secret_local.ts'); + } + } + + // local devevelopers can use localhost or hazmapper.local but + // hazmapper.local has been preferred in the past as TAPIS only supported it as a frame ancestor + // then (i.e. it allows for point cloud iframe preview) + const clientId = /^localhost/.test(hostname) + ? 'XgCBlhfAaqfv7jTu3NRc4IJDGdwa' + : 'Eb9NCCtWkZ83c01UbIAITFvhD9ka'; + + const appConfig: AppConfiguration = { + basePath: basePath, + clientId: clientId, + geoapiBackend: localDevelopmentConfiguration.geoapiBackend, + geoapiUrl: getGeoapiUrl(localDevelopmentConfiguration.geoapiBackend), + designSafeUrl: 'https://agave.designsafe-ci.org/', + designsafePortalUrl: getDesignsafePortalUrl( + DesignSafePortalEnvironment.Dev + ), + mapillary: mapillaryConfig, + taggitUrl: origin + '/taggit-staging', + jwt: localDevelopmentConfiguration.jwt, + }; + appConfig.mapillary.clientId = '5156692464392931'; + appConfig.mapillary.clientSecret = + 'MLY|5156692464392931|6be48c9f4074f4d486e0c42a012b349f'; + appConfig.mapillary.clientToken = + 'MLY|5156692464392931|4f1118aa1b06f051a44217cb56bedf79'; + return appConfig; + } else if ( + /^hazmapper.tacc.utexas.edu/.test(hostname) && + pathname.startsWith('/staging') + ) { + const clientId = basePath.includes('react') + ? 'AhV_h3Ilvrfs1S2Cj10yj82G0Uoa' // "staging-react" client + : 'foitdqFcimPzKZuMhbQ1oyh3Anka'; // "staging client" client + const appConfig: AppConfiguration = { + basePath: basePath, + clientId: clientId, + geoapiBackend: GeoapiBackendEnvironment.Staging, + geoapiUrl: getGeoapiUrl(GeoapiBackendEnvironment.Staging), + designSafeUrl: 'https://agave.designsafe-ci.org/', + designsafePortalUrl: getDesignsafePortalUrl( + DesignSafePortalEnvironment.Dev + ), + mapillary: mapillaryConfig, + taggitUrl: origin + '/taggit-staging', + }; + + appConfig.mapillary.clientId = '4936281379826603'; + appConfig.mapillary.clientSecret = + 'MLY|4936281379826603|cafd014ccd8cfc983e47c69c16082c7b'; + appConfig.mapillary.clientToken = + 'MLY|4936281379826603|f8c4732d3c9d96582b86158feb1c1a7a'; + return appConfig; + } else if ( + /^hazmapper.tacc.utexas.edu/.test(hostname) && + pathname.startsWith('/dev') + ) { + const clientId = basePath.includes('react') + ? '9rWjQLiJb0XPXHicmUh1RUq6rOEa' // "react-dev" client + : 'oEuGsl7xi015wnrEpxIeUmvzc6Qa'; // "dev" client + const appConfig: AppConfiguration = { + basePath: basePath, + clientId: clientId, + geoapiBackend: GeoapiBackendEnvironment.Dev, + geoapiUrl: getGeoapiUrl(GeoapiBackendEnvironment.Dev), + designSafeUrl: 'https://agave.designsafe-ci.org/', + designsafePortalUrl: getDesignsafePortalUrl( + DesignSafePortalEnvironment.Dev + ), + mapillary: mapillaryConfig, + taggitUrl: origin + '/taggit-dev', + }; + + // TODO_REACT mapillary config is currently copy from /staging and not correct for /dev + appConfig.mapillary.clientId = '4936281379826603'; + appConfig.mapillary.clientSecret = + 'MLY|4936281379826603|cafd014ccd8cfc983e47c69c16082c7b'; + appConfig.mapillary.clientToken = + 'MLY|4936281379826603|f8c4732d3c9d96582b86158feb1c1a7a'; + return appConfig; + } else if (/^hazmapper.tacc.utexas.edu/.test(hostname)) { + const clientId = basePath.includes('react') + ? 'XEMnINR8b8hA6kFxE69HVTyoNCga' // "hazmapper-react" client + : 'tMvAiRdcsZ52S_89lCkO4x3d6VMa'; // "hazmapper" client + const appConfig: AppConfiguration = { + basePath: basePath, + clientId: clientId, + geoapiBackend: GeoapiBackendEnvironment.Production, + geoapiUrl: getGeoapiUrl(GeoapiBackendEnvironment.Production), + designSafeUrl: 'https://agave.designsafe-ci.org/', + designsafePortalUrl: getDesignsafePortalUrl( + DesignSafePortalEnvironment.Production + ), + mapillary: mapillaryConfig, + taggitUrl: origin + '/taggit', + }; + + appConfig.mapillary.clientId = '5156692464392931'; + appConfig.mapillary.clientSecret = + 'MLY|5156692464392931|6be48c9f4074f4d486e0c42a012b349f'; + appConfig.mapillary.clientToken = + 'MLY|5156692464392931|4f1118aa1b06f051a44217cb56bedf79'; + return appConfig; + } else { + console.error('Cannot find environment for host name ${hostname}'); + throw new Error('Cannot find environment for host name ${hostname}'); + } + }, []); + return appConfiguration; +}; + +export default useAppConfiguration; diff --git a/react/src/hooks/environment/useBasePath.ts b/react/src/hooks/environment/useBasePath.ts new file mode 100644 index 00000000..72a7bafe --- /dev/null +++ b/react/src/hooks/environment/useBasePath.ts @@ -0,0 +1,28 @@ +import { useMemo } from 'react'; + +/** + * Computes the base path for the application based on the current URL. + */ +const useBasePath = (): string => { + const basePath = useMemo(() => { + // note that path order matters + // as we use startsWith to find a match + const paths: string[] = [ + '/hazmapper-react', + '/staging-react', + '/dev-react', + '/hazmapper', + '/staging', + '/dev', + ]; + const currentPath: string = window.location.pathname; + const base: string | undefined = paths.find((path) => + currentPath.startsWith(path) + ); + return base || '/'; + }, []); + + return basePath; +}; + +export default useBasePath; diff --git a/react/src/hooks/index.ts b/react/src/hooks/index.ts index 62a23442..b232f530 100644 --- a/react/src/hooks/index.ts +++ b/react/src/hooks/index.ts @@ -1 +1,2 @@ export { default as useProjects } from './projects/useProjects'; +export * from './environment'; diff --git a/react/src/hooks/projects/useProjects.ts b/react/src/hooks/projects/useProjects.ts index 2e9a8715..1caa7b49 100644 --- a/react/src/hooks/projects/useProjects.ts +++ b/react/src/hooks/projects/useProjects.ts @@ -6,7 +6,6 @@ const useProjects = (): UseQueryResult => { const query = useGet({ endpoint: '/projects/', key: ['projects'], - baseUrl: 'https://agave.designsafe-ci.org/geo/v2', }); return query; }; diff --git a/react/src/pages/Login/Login.tsx b/react/src/pages/Login/Login.tsx index 7b56a8b3..1b6819c1 100644 --- a/react/src/pages/Login/Login.tsx +++ b/react/src/pages/Login/Login.tsx @@ -3,6 +3,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { useSelector } from 'react-redux'; import { RootState } from '../../redux/store'; import { isTokenValid } from '../../utils/authUtils'; +import { useAppConfiguration } from '../../hooks'; function Login() { const location = useLocation(); @@ -10,6 +11,7 @@ function Login() { const isAuthenticated = useSelector((state: RootState) => isTokenValid(state.auth.token) ); + const configuration = useAppConfiguration(); useEffect(() => { const queryParams = new URLSearchParams(location.search); @@ -23,14 +25,13 @@ function Login() { localStorage.setItem('authState', state); localStorage.setItem('toParam', toParam); - // TODO check for staging/prod - const callbackUrl = `${window.location.origin}/callback`; - - // TODO make auth/server configurable - const client_id = 'RMCJHgW9CwJ6mKjhLTDnUYBo9Hka'; - + const callbackUrl = ( + window.location.origin + + configuration.basePath + + '/callback' + ).replace(/([^:])(\/{2,})/g, '$1/'); // Construct the authentication URL with the client_id, redirect_uri, scope, response_type, and state parameters - const authUrl = `https://agave.designsafe-ci.org/authorize?client_id=${client_id}&redirect_uri=${callbackUrl}&scope=openid&response_type=token&state=${state}`; + const authUrl = `https://agave.designsafe-ci.org/authorize?client_id=${configuration.clientId}&redirect_uri=${callbackUrl}&scope=openid&response_type=token&state=${state}`; window.location.replace(authUrl); } diff --git a/react/src/redux/api/geoapi.ts b/react/src/redux/api/geoapi.ts index fa3fa531..8e1737d2 100644 --- a/react/src/redux/api/geoapi.ts +++ b/react/src/redux/api/geoapi.ts @@ -1,6 +1,7 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react'; import type { RootState } from '../store'; +// TODO_REACT: REMOVE AS NOT USED // TODO: make configurable so can be https://agave.designsafe-ci.org/geo-staging/v2 or https://agave.designsafe-ci.org/geo/v2 // See https://tacc-main.atlassian.net/browse/WG-196 const BASE_URL = 'https://agave.designsafe-ci.org/geo/v2'; diff --git a/react/src/redux/projectsSlice.ts b/react/src/redux/projectsSlice.ts index 49d34e2e..5fbb62b9 100644 --- a/react/src/redux/projectsSlice.ts +++ b/react/src/redux/projectsSlice.ts @@ -1,6 +1,7 @@ import { createSlice } from '@reduxjs/toolkit'; import { geoapi } from './api/geoapi'; +// TODO_REACT: REMOVE AS NOT USED const slice = createSlice({ name: 'projects', initialState: { projects: [] }, diff --git a/react/src/requests.test.ts b/react/src/requests.test.ts new file mode 100644 index 00000000..bf62ef37 --- /dev/null +++ b/react/src/requests.test.ts @@ -0,0 +1,47 @@ +import { getHeaders } from './requests'; +import { ApiService, GeoapiBackendEnvironment } from './types'; +import { + authenticatedUser, + unauthenticatedUser, +} from './__fixtures__/authStateFixtures'; +import { localDevConfiguration } from './__fixtures__/appConfigurationFixture'; + +describe('getHeaders', () => { + it('returns JWT header when using local Geoapi', () => { + const headers = getHeaders( + ApiService.Geoapi, + { + ...localDevConfiguration, + geoapiBackend: GeoapiBackendEnvironment.Local, + }, + authenticatedUser + ); + + expect(headers).toEqual({ + 'X-JWT-Assertion-designsafe': localDevConfiguration.jwt, + }); + }); + + it('returns Authorization header for non-local Geoapi', () => { + const headers = getHeaders( + ApiService.Geoapi, + { + ...localDevConfiguration, + geoapiBackend: GeoapiBackendEnvironment.Production, // Or any other non-local environment + }, + authenticatedUser + ); + expect(headers).toEqual({ + Authorization: `Bearer ${authenticatedUser.token?.token}`, + }); + }); + + it('returns no auth-related headers for unauthenticatedUser', () => { + const headers = getHeaders( + ApiService.Geoapi, + localDevConfiguration, + unauthenticatedUser + ); + expect(headers).toEqual({}); + }); +}); diff --git a/react/src/requests.ts b/react/src/requests.ts index 7ad90c64..a46fc42b 100644 --- a/react/src/requests.ts +++ b/react/src/requests.ts @@ -2,6 +2,50 @@ import axios from 'axios'; import store from './redux/store'; import { AxiosError } from 'axios'; import { useQuery, UseQueryOptions, QueryKey } from 'react-query'; +import { useAppConfiguration } from './hooks'; +import { + ApiService, + AppConfiguration, + AuthState, + GeoapiBackendEnvironment, +} from './types'; + +function getBaseApiUrl( + apiService: ApiService, + configuration: AppConfiguration +): string { + switch (apiService) { + case ApiService.Geoapi: + return configuration.geoapiUrl; + case ApiService.DesignSafe: + return configuration.designSafeUrl; + case ApiService.Tapis: + // Tapis and DesignSafe are currently the same + return configuration.designSafeUrl; + default: + throw new Error('Unsupported api service Type.'); + } +} + +export function getHeaders( + apiService: ApiService, + configuration: AppConfiguration, + auth: AuthState +) { + // TODO_REACT add mapillary support + if (auth.token && apiService !== ApiService.Mapillary) { + //Add auth information in header for DesignSafe, Tapis, Geoapi for logged in users + if ( + apiService === ApiService.Geoapi && + configuration.geoapiBackend === GeoapiBackendEnvironment.Local + ) { + // Use JWT in request header because local geoapi API is not behind ws02 + return { 'X-JWT-Assertion-designsafe': configuration.jwt }; + } + return { Authorization: `Bearer ${auth.token.token}` }; + } + return {}; +} type UseGetParams = { endpoint: string; @@ -10,25 +54,31 @@ type UseGetParams = { UseQueryOptions, 'queryKey' | 'queryFn' >; - baseUrl: string; + apiService?: ApiService; }; export function useGet({ endpoint, key, options = {}, - baseUrl, + apiService = ApiService.Geoapi, }: UseGetParams) { const client = axios; const state = store.getState(); - const token = state.auth.token?.token; - // change to prod const { baseUrl } = useConfig(); + const configuration = useAppConfiguration(); + + const baseUrl = getBaseApiUrl(apiService, configuration); + const headers = getHeaders(apiService, configuration, state.auth); + + /* TODO_REACT Send analytics-related params to projects endpoint only (until we use headers + again in https://tacc-main.atlassian.net/browse/WG-192) */ + const getUtil = async () => { const request = await client.get( `${baseUrl}${endpoint}`, { - headers: { Authorization: `Bearer ${token}` }, + headers: headers, } ); return request.data; diff --git a/react/src/secret_local.example.ts b/react/src/secret_local.example.ts new file mode 100644 index 00000000..396ba952 --- /dev/null +++ b/react/src/secret_local.example.ts @@ -0,0 +1,9 @@ +import { GeoapiBackendEnvironment, LocalAppConfiguration } from './types'; + +// prettier-ignore +const jwt = 'INSERT YOUR JWT HERE; See README '; + +export const localDevelopmentConfiguration: LocalAppConfiguration = { + jwt: jwt, + geoapiBackend: GeoapiBackendEnvironment.Production, +}; diff --git a/react/src/types/environment.ts b/react/src/types/environment.ts new file mode 100644 index 00000000..d1ae074a --- /dev/null +++ b/react/src/types/environment.ts @@ -0,0 +1,95 @@ +/** + * Environment for Geoapi Backend + */ +export enum GeoapiBackendEnvironment { + Production = 'production', + Staging = 'staging', + Dev = 'dev', + Local = 'local', +} + +/** + * Environment for Geoapi Backend + */ +export enum DesignSafePortalEnvironment { + Production = 'production', + Dev = 'dev' /* DesignSafe has 2 deployed environments: prod and dev. This dev is comparable to Geoapi's staging */, +} + +/** + * Known Apis + */ +export enum ApiService { + /* Geoapi api */ + Geoapi = 'geoapi', + + /* DesignSafe api - for project listings */ + DesignSafe = 'designsafe', + + /* Tapis api - for system listings and file operations */ + Tapis = 'tapis', + + /* Mapillary */ + Mapillary = 'mapillary', +} + +/** + * Configuration settings for local development. + * + * These can be configured by developer in secret_local.ts (see README) + * + */ +export interface LocalAppConfiguration { + /* Developer's JWT token used for authentication during local development. */ + jwt: string; + + /* The type of backend environment (production, staging, development, or local) */ + geoapiBackend: GeoapiBackendEnvironment; +} + +/** + * Mapillary configuration + */ +export interface MapillaryConfiguration { + authUrl: string; + tokenUrl: string; + apiUrl: string; + tileUrl: string; + scope: string; + clientSecret: string; + clientId: string; + clientToken: string; +} + +/** + * Configuration for the application + * related to Geoapi backend, auth and other services + */ +export interface AppConfiguration { + /** Base URL path for the application. */ + basePath: string; + + /** Client ID used for Tapis authentication. */ + clientId: string; + + /* The type of backend environment */ + geoapiBackend: GeoapiBackendEnvironment; + + /** URL for the GeoAPI service. */ + geoapiUrl: string; + + /** URL for the DesignSafe/tapis API. */ + designSafeUrl: string; + + /** URL for the DesignSafe portal. */ + designsafePortalUrl: string; + + /** Mapillary related configuration */ + mapillary: MapillaryConfiguration; + + /** URL for taggit */ + taggitUrl: string; + + /** Optional JWT token used for development with local geoapi service. */ + jwt?: string; +} diff --git a/react/src/types/index.ts b/react/src/types/index.ts index a7280fb1..8967d955 100644 --- a/react/src/types/index.ts +++ b/react/src/types/index.ts @@ -7,3 +7,4 @@ export type { } from './feature'; export type { Project } from './projects'; export type { AuthState, AuthenticatedUser, AuthToken } from './auth'; +export * from './environment'; diff --git a/react/vite.config.ts b/react/vite.config.ts index edb68196..98196463 100644 --- a/react/vite.config.ts +++ b/react/vite.config.ts @@ -1,99 +1,13 @@ -import { defineConfig, loadEnv } from 'vite'; +import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; -// Retrieves the target back. -// Used for getting a dynamic backend for local development. -function getGeoapiUrl(backend: string): string { - if (backend === 'development') { - return 'http://localhost:8888'; - } else if (backend === 'staging') { - return 'https://agave.designsafe-ci.org/geo-staging/v2'; - } else if (backend === 'production') { - return 'https://agave.designsafe-ci.org/geo/v2'; - } else { - throw new Error( - 'Unsupported TARGET/GEOAPI_BACKEND Type. Please check the .env file.' - ); - } -} - -function getDesignsafePortalUrl(backend: string): string { - if (backend === 'production') { - return 'https://www.designsafe-ci.org/'; - } else { - return 'https://designsafeci-dev.tacc.utexas.edu/'; - } -} - // https://vitejs.dev/config/ export default defineConfig(({ command, mode }) => { // eslint-disable-line - const envFile = loadEnv(mode, process.cwd(), ''); - const targetEnvironment = envFile.TARGET; - const env = { - designSafeUrl: 'https://agave.designsafe-ci.org/', - backend: envFile.GEOAPI_BACKEND, - geoapiUrl: '', - designsafePortalUrl: '', - clientId: '', - host: '', - baseHref: '', - jwt: '', - mapillaryAuthUrl: 'https://www.mapillary.com/connect', - mapillaryTokenUrl: 'https://graph.mapillary.com/token', - mapillaryApiUrl: 'https://graph.mapillary.com/', - mapillaryTileUrl: 'https://tiles.mapillary.com/', - mapillaryScope: - 'user:email+user:read+user:write+public:write+public:upload+private:read+private:write+private:upload', - mapillaryClientSecret: '', - mapillaryClientId: '', - mapillaryClientToken: '', - }; - - if (targetEnvironment === 'production') { - env.geoapiUrl = getGeoapiUrl(targetEnvironment); - env.designsafePortalUrl = getDesignsafePortalUrl(targetEnvironment); - env.clientId = 'tMvAiRdcsZ52S_89lCkO4x3d6VMa'; - env.host = 'hazmapper.utexas.edu/hazmapper/'; - env.baseHref = '/hazmapper/'; - env.mapillaryClientId = '5156692464392931'; - env.mapillaryClientSecret = - 'MLY|5156692464392931|6be48c9f4074f4d486e0c42a012b349f'; - env.mapillaryClientToken = - 'MLY|5156692464392931|4f1118aa1b06f051a44217cb56bedf79'; - } else if (targetEnvironment === 'staging') { - env.geoapiUrl = getGeoapiUrl(targetEnvironment); - env.designsafePortalUrl = getDesignsafePortalUrl(targetEnvironment); - env.clientId = 'foitdqFcimPzKZuMhbQ1oyh3Anka'; - env.host = 'hazmapper.utexas.edu/staging/'; - env.baseHref = '/staging/'; - env.mapillaryClientSecret = - 'MLY|4936281379826603|cafd014ccd8cfc983e47c69c16082c7b'; - env.mapillaryClientId = '4936281379826603'; - env.mapillaryClientToken = - 'MLY|4936281379826603|f8c4732d3c9d96582b86158feb1c1a7a'; - } else { - env.geoapiUrl = getGeoapiUrl(envFile.GEOAPI_BACKEND); - env.designsafePortalUrl = getDesignsafePortalUrl(envFile.GEOAPI_BACKEND); - env.clientId = 'Eb9NCCtWkZ83c01UbIAITFvhD9ka'; - env.host = 'hazmapper.local'; - env.baseHref = '/'; - env.mapillaryClientSecret = - 'MLY|5156692464392931|6be48c9f4074f4d486e0c42a012b349f'; - env.mapillaryClientId = '5156692464392931'; - env.mapillaryClientToken = - 'MLY|5156692464392931|4f1118aa1b06f051a44217cb56bedf79'; - env.jwt = envFile.BACKEND === 'development' && envFile.JWT; - } - return { plugins: [react()], server: { port: 4200, - base: env.baseHref, - host: env.host, - }, - define: { - 'process.env': env, + host: 'localhost', }, }; });