diff --git a/react/package-lock.json b/react/package-lock.json
index 88d0fde5..12977a53 100644
--- a/react/package-lock.json
+++ b/react/package-lock.json
@@ -24,6 +24,7 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react-hooks": "^4.6.0",
"formik": "^2.4.5",
+ "jwt-decode": "^4.0.0",
"leaflet": "^1.9.3",
"postcss-nesting": "^12.0.3",
"prettier": "^2.7.1",
@@ -3363,9 +3364,9 @@
}
},
"node_modules/@tacc/core-styles": {
- "version": "2.24.1",
- "resolved": "https://registry.npmjs.org/@tacc/core-styles/-/core-styles-2.24.1.tgz",
- "integrity": "sha512-agJZ3+86tfj+yM4z+hoW5nTT6gBJpi6kL6KASBVgm5lEiApIrPJN2BKaQDNKcht12sPBxyNtFIGtfGrPTESirQ==",
+ "version": "2.30.1",
+ "resolved": "https://registry.npmjs.org/@tacc/core-styles/-/core-styles-2.30.1.tgz",
+ "integrity": "sha512-VdtVaz41bBBUhPkIS+iXWVCbxoY5yTdm2PIU9hZBh2I9cJF4jbTAnpmxSGPRDMTYLCljDNP2scL1fLjwSY/8+Q==",
"bin": {
"core-styles": "src/cli.js"
},
@@ -3379,11 +3380,12 @@
"js-yaml": "^4.1.0",
"merge-lite": "^1.0.2",
"node-cmd": "^5.0.0",
- "postcss": "^8.4.18",
+ "postcss": "^8.4.38",
"postcss-banner": "^4.0.1",
"postcss-cli": "^10.0.0",
"postcss-extend": "^1.0.5",
"postcss-import": "^15.0.0",
+ "postcss-mixins": "^10.0.1",
"postcss-preset-env": "^7.8.3",
"postcss-replace": "^2.0.1"
}
@@ -5998,11 +6000,11 @@
}
},
"node_modules/axios": {
- "version": "1.6.7",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
- "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
+ "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"dependencies": {
- "follow-redirects": "^1.15.4",
+ "follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
@@ -6266,11 +6268,11 @@
}
},
"node_modules/braces": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
- "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dependencies": {
- "fill-range": "^7.0.1"
+ "fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -6384,6 +6386,15 @@
"node": ">=6"
}
},
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "peer": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/caniuse-api": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
@@ -8483,9 +8494,9 @@
}
},
"node_modules/fill-range": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
- "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -8527,9 +8538,9 @@
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="
},
"node_modules/follow-redirects": {
- "version": "1.15.5",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
- "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
@@ -11739,6 +11750,14 @@
"node": ">=4.0"
}
},
+ "node_modules/jwt-decode": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
+ "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/kdbush": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz",
@@ -12545,9 +12564,9 @@
}
},
"node_modules/picocolors": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
+ "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -12670,9 +12689,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.35",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
- "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
+ "version": "8.4.41",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
+ "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
"funding": [
{
"type": "opencollective",
@@ -12689,8 +12708,8 @@
],
"dependencies": {
"nanoid": "^3.3.7",
- "picocolors": "^1.0.0",
- "source-map-js": "^1.0.2"
+ "picocolors": "^1.0.1",
+ "source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -13283,6 +13302,25 @@
"postcss": "^8.0.0"
}
},
+ "node_modules/postcss-js": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+ "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+ "peer": true,
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
"node_modules/postcss-lab-function": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz",
@@ -13484,6 +13522,34 @@
"postcss": "^8.2.15"
}
},
+ "node_modules/postcss-mixins": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-mixins/-/postcss-mixins-10.0.1.tgz",
+ "integrity": "sha512-5+cI9r8L5ChegVsLM9pXa53Ft03Mt9xAq+kvzqfrUHGPCArVGOfUvmQK2CLP3XWWP2dqxDLQI+lIcXG+GTqOBQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "peer": true,
+ "dependencies": {
+ "fast-glob": "^3.3.2",
+ "postcss-js": "^4.0.1",
+ "postcss-simple-vars": "^7.0.1",
+ "sugarss": "^4.0.1"
+ },
+ "engines": {
+ "node": "^18.0 || >= 20.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
"node_modules/postcss-nesting": {
"version": "12.0.4",
"resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.0.4.tgz",
@@ -13963,6 +14029,22 @@
"node": ">=4"
}
},
+ "node_modules/postcss-simple-vars": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-simple-vars/-/postcss-simple-vars-7.0.1.tgz",
+ "integrity": "sha512-5GLLXaS8qmzHMOjVxqkk1TZPf1jMqesiI7qLhnlyERalG0sMbHIbJqrcnrpmZdKCLglHnRHoEBB61RtGTsj++A==",
+ "peer": true,
+ "engines": {
+ "node": ">=14.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.1"
+ }
+ },
"node_modules/postcss-svgo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz",
@@ -14947,9 +15029,9 @@
}
},
"node_modules/source-map-js": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
- "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
+ "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"engines": {
"node": ">=0.10.0"
}
@@ -15170,6 +15252,22 @@
"postcss": "^8.2.15"
}
},
+ "node_modules/sugarss": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-4.0.1.tgz",
+ "integrity": "sha512-WCjS5NfuVJjkQzK10s8WOBY+hhDxxNt/N6ZaGwxFZ+wN3/lKKFSaaKUNecULcTTvE4urLcKaZFQD8vO0mOZujw==",
+ "peer": true,
+ "engines": {
+ "node": ">=12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.3.3"
+ }
+ },
"node_modules/supercluster": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz",
@@ -15770,9 +15868,9 @@
}
},
"node_modules/vite": {
- "version": "3.2.8",
- "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.8.tgz",
- "integrity": "sha512-EtQU16PLIJpAZol2cTLttNP1mX6L0SyI0pgQB1VOoWeQnMSvtiwovV3D6NcjN8CZQWWyESD2v5NGnpz5RvgOZA==",
+ "version": "3.2.10",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-3.2.10.tgz",
+ "integrity": "sha512-Dx3olBo/ODNiMVk/cA5Yft9Ws+snLOXrhLtrI3F4XLt4syz2Yg8fayZMWScPKoz12v5BUv7VEmQHnsfpY80fYw==",
"dev": true,
"dependencies": {
"esbuild": "^0.15.9",
@@ -16052,9 +16150,9 @@
}
},
"node_modules/ws": {
- "version": "8.16.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
- "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"dev": true,
"engines": {
"node": ">=10.0.0"
diff --git a/react/package.json b/react/package.json
index 2256fc7b..c6a9bd14 100644
--- a/react/package.json
+++ b/react/package.json
@@ -48,6 +48,7 @@
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react-hooks": "^4.6.0",
"formik": "^2.4.5",
+ "jwt-decode": "^4.0.0",
"leaflet": "^1.9.3",
"postcss-nesting": "^12.0.3",
"prettier": "^2.7.1",
diff --git a/react/src/__fixtures__/appConfigurationFixture.ts b/react/src/__fixtures__/appConfigurationFixture.ts
index 933dbf37..374d360b 100644
--- a/react/src/__fixtures__/appConfigurationFixture.ts
+++ b/react/src/__fixtures__/appConfigurationFixture.ts
@@ -4,9 +4,6 @@ import {
MapillaryConfiguration,
} from '../types';
-const clientId = 'abc_123_JWT';
-const jwtId = 'abc_123_client_id';
-
export const mapillaryConfig: MapillaryConfiguration = {
authUrl: 'https://www.mapillary.com/connect',
tokenUrl: 'https://graph.mapillary.com/token',
@@ -21,12 +18,9 @@ export const mapillaryConfig: MapillaryConfiguration = {
export const localDevConfiguration: AppConfiguration = {
basePath: '/',
- clientId: clientId,
geoapiBackend: GeoapiBackendEnvironment.Local,
geoapiUrl: 'http://localhost:8888',
- designSafeUrl: 'https://agave.designsafe-ci.org/',
- designsafePortalUrl: 'https://designsafeci-dev.tacc.utexas.edu/',
+ designsafePortalUrl: 'https://designsafeci-dev.tacc.utexas.edu',
mapillary: mapillaryConfig,
taggitUrl: 'http://localhost:4200/taggit-staging',
- jwt: jwtId,
};
diff --git a/react/src/__fixtures__/authStateFixtures.ts b/react/src/__fixtures__/authStateFixtures.ts
index fdc63fe9..4d6215b1 100644
--- a/react/src/__fixtures__/authStateFixtures.ts
+++ b/react/src/__fixtures__/authStateFixtures.ts
@@ -1,13 +1,15 @@
import { AuthState } from '../types';
+// Convert the timestamp to a Date object
+const expiresAtDate = new Date(3153600000000); //2070
+
export const authenticatedUser: AuthState = {
user: {
username: 'user',
- email: 'user@user.com',
},
authToken: {
token: 'auth-token',
- expires: 3153600000000, // 2070
+ expiresAt: expiresAtDate.toISOString(),
},
};
diff --git a/react/src/components/CreateMapModal/CreateMapModal.test.tsx b/react/src/components/CreateMapModal/CreateMapModal.test.tsx
index 585e99e0..c8beaa2d 100644
--- a/react/src/components/CreateMapModal/CreateMapModal.test.tsx
+++ b/react/src/components/CreateMapModal/CreateMapModal.test.tsx
@@ -6,14 +6,16 @@ import {
screen,
waitFor,
} from '@testing-library/react';
+
import CreateMapModal from './CreateMapModal';
import { BrowserRouter as Router } from 'react-router-dom';
+import { act } from 'react-dom/test-utils';
import { QueryClient, QueryClientProvider } from 'react-query';
jest.mock('../../hooks/user/useAuthenticatedUser', () => ({
__esModule: true,
default: () => ({
- data: { username: 'mockUser', email: 'mockUser@example.com' },
+ data: { username: 'mockUser' },
isLoading: false,
error: null,
}),
@@ -45,14 +47,16 @@ jest.mock('react-router-dom', () => ({
const toggleMock = jest.fn();
const queryClient = new QueryClient();
-const renderComponent = (isOpen = true) => {
- render(
-
-
-
-
-
- );
+const renderComponent = async (isOpen = true) => {
+ await act(async () => {
+ render(
+
+
+
+
+
+ );
+ });
};
describe('CreateMapModal', () => {
@@ -60,23 +64,27 @@ describe('CreateMapModal', () => {
cleanup();
});
- test('renders the modal when open', () => {
- renderComponent();
- expect(screen.getByText(/Create a New Map/)).toBeTruthy();
+ test('renders the modal when open', async () => {
+ await renderComponent();
+ await waitFor(() => {
+ expect(screen.getByText(/Create a New Map/)).toBeTruthy();
+ });
});
test('submits form data successfully', async () => {
- renderComponent();
- fireEvent.change(screen.getByTestId('name-input'), {
- target: { value: 'Success Map' },
+ await renderComponent();
+ await act(async () => {
+ fireEvent.change(screen.getByTestId('name-input'), {
+ target: { value: 'Success Map' },
+ });
+ fireEvent.change(screen.getByLabelText(/Description/), {
+ target: { value: 'A successful map' },
+ });
+ fireEvent.change(screen.getByLabelText(/Custom File Name/), {
+ target: { value: 'success-file' },
+ });
+ fireEvent.click(screen.getByRole('button', { name: /Create/ }));
});
- fireEvent.change(screen.getByLabelText(/Description/), {
- target: { value: 'A successful map' },
- });
- fireEvent.change(screen.getByLabelText(/Custom File Name/), {
- target: { value: 'success-file' },
- });
- fireEvent.click(screen.getByRole('button', { name: /Create/ }));
await waitFor(() => {
expect(mockNavigate).toHaveBeenCalledWith('/project/123');
@@ -84,17 +92,19 @@ describe('CreateMapModal', () => {
});
test('displays error message on submission error', async () => {
- renderComponent();
- fireEvent.change(screen.getByTestId('name-input'), {
- target: { value: 'Error Map' },
- });
- fireEvent.change(screen.getByLabelText(/Description/), {
- target: { value: 'A map with an error' },
- });
- fireEvent.change(screen.getByLabelText(/Custom File Name/), {
- target: { value: 'error-file' },
+ await renderComponent();
+ await act(async () => {
+ fireEvent.change(screen.getByTestId('name-input'), {
+ target: { value: 'Error Map' },
+ });
+ fireEvent.change(screen.getByLabelText(/Description/), {
+ target: { value: 'A map with an error' },
+ });
+ fireEvent.change(screen.getByLabelText(/Custom File Name/), {
+ target: { value: 'error-file' },
+ });
+ fireEvent.click(screen.getByRole('button', { name: /Create/ }));
});
- fireEvent.click(screen.getByRole('button', { name: /Create/ }));
await waitFor(() => {
expect(
diff --git a/react/src/components/Projects/ProjectListing.tsx b/react/src/components/Projects/ProjectListing.tsx
index c082f9a3..7f5100f8 100644
--- a/react/src/components/Projects/ProjectListing.tsx
+++ b/react/src/components/Projects/ProjectListing.tsx
@@ -7,9 +7,15 @@ import {
} from '../../hooks';
import { Button, LoadingSpinner } from '../../core-components';
import CreateMapModal from '../CreateMapModal/CreateMapModal';
+import { useNavigate } from 'react-router-dom';
export const ProjectListing: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
+ const navigate = useNavigate();
+
+ const navigateToProject = (projectId) => {
+ navigate(`/project/${projectId}`);
+ };
const toggleModal = () => {
setIsModalOpen(!isModalOpen);
@@ -49,7 +55,7 @@ export const ProjectListing: React.FC = () => {
{projectsData?.map((proj) => (
-
+
navigateToProject(proj.uuid)}>
{proj.name} |
{proj.ds_project_id} {proj.ds_project_title}
diff --git a/react/src/constants/routes.ts b/react/src/constants/routes.ts
index 274aab65..8599525b 100644
--- a/react/src/constants/routes.ts
+++ b/react/src/constants/routes.ts
@@ -3,5 +3,5 @@ export const LOGIN = '/login';
export const LOGOUT = '/logout';
export const PUBLIC_PROJECT = '/project-public/:projectUUID';
export const PROJECT = '/project/:projectUUID';
-export const CALLBACK = '/callback';
+export const CALLBACK = '/handle-login';
export const STREETVIEW_CALLBACK = '/streetview/callback';
diff --git a/react/src/hooks/environment/useAppConfiguration.ts b/react/src/hooks/environment/useAppConfiguration.ts
index 59d34341..27089ee0 100644
--- a/react/src/hooks/environment/useAppConfiguration.ts
+++ b/react/src/hooks/environment/useAppConfiguration.ts
@@ -18,12 +18,14 @@ function getGeoapiUrl(backend: GeoapiBackendEnvironment): string {
switch (backend) {
case GeoapiBackendEnvironment.Local:
return 'http://localhost:8888';
+ case GeoapiBackendEnvironment.Experimental:
+ return 'https://hazmapper.tacc.utexas.edu/geoapi-experimental';
case GeoapiBackendEnvironment.Dev:
- return 'https://agave.designsafe-ci.org/geo-dev/v2';
+ return 'https://hazmapper.tacc.utexas.edu/geoapi-dev';
case GeoapiBackendEnvironment.Staging:
- return 'https://agave.designsafe-ci.org/geo-staging/v2';
+ return 'https://hazmapper.tacc.utexas.edu/geoapi-staging';
case GeoapiBackendEnvironment.Production:
- return 'https://agave.designsafe-ci.org/geo/v2';
+ return 'https://hazmapper.tacc.utexas.edu/geoapi';
default:
throw new Error(
'Unsupported TARGET/GEOAPI_BACKEND Type. Please check the .env file.'
@@ -36,9 +38,13 @@ function getGeoapiUrl(backend: GeoapiBackendEnvironment): string {
*/
function getDesignsafePortalUrl(backend: DesignSafePortalEnvironment): string {
if (backend === DesignSafePortalEnvironment.Production) {
- return 'https://www.designsafe-ci.org/';
+ return 'https://www.designsafe-ci.org';
+ } else if (backend === DesignSafePortalEnvironment.Next) {
+ return 'https://designsafeci-next.tacc.utexas.edu';
+ } else if (backend === DesignSafePortalEnvironment.Dev) {
+ return 'https://designsafeci-dev.tacc.utexas.edu';
} else {
- return 'https://designsafeci-dev.tacc.utexas.edu/';
+ throw new Error('Unsupported DS environment');
}
}
@@ -62,40 +68,15 @@ export const useAppConfiguration = (): AppConfiguration => {
};
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 =
@@ -107,15 +88,10 @@ export const useAppConfiguration = (): AppConfiguration => {
/^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
),
@@ -133,15 +109,10 @@ export const useAppConfiguration = (): AppConfiguration => {
/^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
),
@@ -157,15 +128,10 @@ export const useAppConfiguration = (): AppConfiguration => {
'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
),
diff --git a/react/src/hooks/index.ts b/react/src/hooks/index.ts
index 859181ec..6f530573 100644
--- a/react/src/hooks/index.ts
+++ b/react/src/hooks/index.ts
@@ -4,3 +4,4 @@ export { useTileServers } from './tileServers/useTileServers';
export { default as useSystems } from './systems/useSystems';
export * from './environment';
export * from './projects';
+export * from './user';
diff --git a/react/src/hooks/projects/useProjects.ts b/react/src/hooks/projects/useProjects.ts
index 10a22ec2..c37787e4 100644
--- a/react/src/hooks/projects/useProjects.ts
+++ b/react/src/hooks/projects/useProjects.ts
@@ -39,7 +39,7 @@ export const useProject = ({
export const useDsProjects =
(): UseQueryResult => {
const query = useGet({
- endpoint: `projects/v2/`,
+ endpoint: `/api/projects/v2/`,
key: ['projectsv2'],
apiService: ApiService.DesignSafe,
});
diff --git a/react/src/hooks/systems/useSystems.ts b/react/src/hooks/systems/useSystems.ts
index 4732cebd..99ddf609 100644
--- a/react/src/hooks/systems/useSystems.ts
+++ b/react/src/hooks/systems/useSystems.ts
@@ -4,9 +4,9 @@ import { useGet } from '../../requests';
const useSystems = (): UseQueryResult => {
return useGet({
- endpoint: '/systems/v2?type=STORAGE',
- key: ['systemsv2'],
- apiService: ApiService.DesignSafe,
+ endpoint: '/v3/systems/?listType=ALL',
+ key: ['systemsv3'],
+ apiService: ApiService.Tapis,
transform: (data) => data.result,
});
};
diff --git a/react/src/hooks/user/index.ts b/react/src/hooks/user/index.ts
new file mode 100644
index 00000000..aede95c7
--- /dev/null
+++ b/react/src/hooks/user/index.ts
@@ -0,0 +1,2 @@
+export { useEnsureAuthenticatedUserHasValidTapisToken } from './useEnsureAuthenticatedUserHasValidTapisToken';
+export { default as useAuthenticatedUser } from './useAuthenticatedUser';
diff --git a/react/src/hooks/user/useAuthenticatedUser.ts b/react/src/hooks/user/useAuthenticatedUser.ts
index 46754c8e..d884c411 100644
--- a/react/src/hooks/user/useAuthenticatedUser.ts
+++ b/react/src/hooks/user/useAuthenticatedUser.ts
@@ -1,17 +1,22 @@
-import { UseQueryResult } from 'react-query';
-import { ApiService, AuthenticatedUser } from '../../types';
-import { useGet } from '../../requests';
+import { useSelector } from 'react-redux';
+import { RootState } from '../../redux/store';
+import { AuthenticatedUser } from '../../types';
-const useAuthenticatedUser = (): UseQueryResult => {
- return useGet({
- endpoint: '/oauth2/userinfo?schema=openid',
- key: ['username'],
- apiService: ApiService.Tapis,
- transform: (data) => ({
- username: data.name,
- email: data.email,
- }),
- });
+type SuccessResult = {
+ data: T;
+ isLoading: false;
+ error: null;
+};
+
+// TODO remove this placeholder hook
+const useAuthenticatedUser = (): SuccessResult => {
+ let username = useSelector((state: RootState) => state.auth.user?.username);
+
+ if (!username) {
+ username = '';
+ }
+
+ return { data: { username }, isLoading: false, error: null };
};
export default useAuthenticatedUser;
diff --git a/react/src/hooks/user/useEnsureAuthenticatedUserHasValidTapisToken.ts b/react/src/hooks/user/useEnsureAuthenticatedUserHasValidTapisToken.ts
new file mode 100644
index 00000000..be795b28
--- /dev/null
+++ b/react/src/hooks/user/useEnsureAuthenticatedUserHasValidTapisToken.ts
@@ -0,0 +1,15 @@
+import { useNavigate, useLocation } from 'react-router-dom';
+import { useSelector } from 'react-redux';
+import { isTokenValid } from '../../utils/authUtils';
+import { RootState } from '../../redux/store';
+
+export function useEnsureAuthenticatedUserHasValidTapisToken() {
+ const navigate = useNavigate();
+ const location = useLocation();
+ const authToken = useSelector((state: RootState) => state.auth.authToken);
+
+ // if user has auth token, ensure its valid and if not, redirect to login
+ if (authToken && !isTokenValid(authToken)) {
+ navigate(`/login?to=${encodeURIComponent(location.pathname)}`);
+ }
+}
diff --git a/react/src/pages/Callback/Callback.tsx b/react/src/pages/Callback/Callback.tsx
index 26fa78a9..63913b08 100644
--- a/react/src/pages/Callback/Callback.tsx
+++ b/react/src/pages/Callback/Callback.tsx
@@ -1,7 +1,9 @@
import React, { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useDispatch } from 'react-redux';
-import { loginSuccess, logout } from '../../redux/authSlice';
+import { jwtDecode } from 'jwt-decode';
+import { loginSuccess } from '../../redux/authSlice';
+import { AuthenticatedUser, AuthToken } from '../../types';
export default function CallbackPage() {
const dispatch = useDispatch();
@@ -9,31 +11,23 @@ export default function CallbackPage() {
const navigate = useNavigate();
useEffect(() => {
- // Parse the query parameters from the URL
- const params = new URLSearchParams(location.hash.slice(1));
-
- // Check the state value against the expected value
- const state = params.get('state');
- const expectedState = localStorage.getItem('authState');
- if (state !== expectedState) {
- console.error('State for callback is incorrect. Send to login');
+ /* TODO use hash instead of search https://tacc-main.atlassian.net/browse/WG-367 */
+ const params = new URLSearchParams(location.search);
+ const redirectTo = localStorage.getItem('toParam') || '/';
+ const token = params.get('access_token');
+ const expiresAt = params.get('expires_at');
- // Redirect the user to the login page
- dispatch(logout());
- return;
- }
+ if (token && expiresAt) {
+ const username = jwtDecode(token)['tapis/username'];
- const redirectTo = localStorage.getItem('toParam') || '/';
+ const authToken: AuthToken = { token, expiresAt };
+ const user: AuthenticatedUser = { username };
- const token = params.get('access_token');
- const expiresIn = params.get('expires_in');
- if (token && expiresIn) {
- const expires = Date.now() + parseInt(expiresIn) * 1000;
- // Save the token to the Redux store
- dispatch(loginSuccess({ token, expires }));
+ // Save the token/username to the Redux store
+ dispatch(loginSuccess({ user, authToken }));
navigate(redirectTo);
}
- }, [dispatch]);
+ }, [dispatch, location, navigate]);
- return Logging in... ;
+ return Logging in. ;
}
diff --git a/react/src/pages/Login/Login.test.tsx b/react/src/pages/Login/Login.test.tsx
index 5bac8d82..f7a13d8c 100644
--- a/react/src/pages/Login/Login.test.tsx
+++ b/react/src/pages/Login/Login.test.tsx
@@ -7,6 +7,23 @@ import { QueryClientProvider } from 'react-query';
import { testQueryClient } from '../../testUtil';
import { MemoryRouter } from 'react-router';
+beforeAll(() => {
+ const mockLocation = {
+ href: 'http://localhost:4200/login',
+ hostname: 'localhost',
+ pathname: '/login',
+ assign: jest.fn(),
+ replace: jest.fn(),
+ // You can add other properties if needed
+ };
+
+ jest.spyOn(window, 'location', 'get').mockReturnValue(mockLocation as any);
+});
+
+afterAll(() => {
+ jest.restoreAllMocks(); // Restore the original window.location after tests
+});
+
test('renders login', async () => {
const { getByText } = render(
@@ -18,7 +35,11 @@ test('renders login', async () => {
);
expect(getByText(/Logging in/)).toBeDefined();
+
await waitFor(() => {
- expect(localStorage.getItem('authState')).not.toBeNull();
+ // Check that localStorage was set with the correct "toParam"
+ expect(localStorage.getItem('toParam')).toBe('/');
+ // Check that the mocked location was set correctly
+ expect(window.location.href).toContain('geoapi');
});
});
diff --git a/react/src/pages/Login/Login.tsx b/react/src/pages/Login/Login.tsx
index 16c94393..cca182fe 100644
--- a/react/src/pages/Login/Login.tsx
+++ b/react/src/pages/Login/Login.tsx
@@ -20,20 +20,11 @@ function Login() {
if (isAuthenticated) {
navigate(toParam);
} else {
- const state = Math.random().toString(36);
- // Save the authState parameter to localStorage
- localStorage.setItem('authState', state);
+ // Save the "to" parameter to localStorage
localStorage.setItem('toParam', toParam);
- 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=${configuration.clientId}&redirect_uri=${callbackUrl}&scope=openid&response_type=token&state=${state}`;
-
- window.location.replace(authUrl);
+ const GEOAPI_AUTH_URL = `${configuration.geoapiUrl}/auth/login?to=${toParam}`;
+ window.location.href = GEOAPI_AUTH_URL;
}
}, []);
diff --git a/react/src/redux/authSlice.ts b/react/src/redux/authSlice.ts
index 3bc1822c..cee8bb2e 100644
--- a/react/src/redux/authSlice.ts
+++ b/react/src/redux/authSlice.ts
@@ -1,17 +1,13 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import {
- getTokenFromLocalStorage,
- setTokenToLocalStorage,
- removeTokenFromLocalStorage,
+ getAuthenticatedUserFromLocalStorage,
+ setAuthenticatedUserFromLocalStorage,
+ removeAuthenticatedUserFromLocalStorage,
} from '../utils/authUtils';
-import { AuthState, AuthenticatedUser } from '../types';
+import { AuthenticatedUser, AuthToken } from '../types';
-// TODO consider moving to ../types/
// check local storage for our initial state
-const initialState: AuthState = {
- authToken: getTokenFromLocalStorage(),
- user: null,
-};
+const initialState = getAuthenticatedUserFromLocalStorage();
const authSlice = createSlice({
name: 'auth',
@@ -19,25 +15,21 @@ const authSlice = createSlice({
reducers: {
loginSuccess(
state,
- action: PayloadAction<{ token: string; expires: number }>
+ action: PayloadAction<{ user: AuthenticatedUser; authToken: AuthToken }>
) {
- state.authToken = {
- token: action.payload.token,
- expires: action.payload.expires,
+ state = {
+ user: action.payload.user,
+ authToken: action.payload.authToken,
};
// save to local storage
- setTokenToLocalStorage(state.authToken);
+ setAuthenticatedUserFromLocalStorage(state);
},
logout(state) {
state.user = null;
state.authToken = null;
//remove from local storage
- removeTokenFromLocalStorage();
- },
-
- setUser(state, action: PayloadAction<{ user: AuthenticatedUser }>) {
- state.user = action.payload.user;
+ removeAuthenticatedUserFromLocalStorage();
},
},
});
diff --git a/react/src/requests.test.ts b/react/src/requests.test.ts
index 5aeb50d4..95f41bda 100644
--- a/react/src/requests.test.ts
+++ b/react/src/requests.test.ts
@@ -1,47 +1,20 @@
import { getHeaders } from './requests';
-import { ApiService, GeoapiBackendEnvironment } from './types';
+import { ApiService } 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
- );
+ it('returns Authorization header for Geoapi', () => {
+ const headers = getHeaders(ApiService.Geoapi, authenticatedUser);
expect(headers).toEqual({
- Authorization: `Bearer ${authenticatedUser.authToken?.token}`,
+ 'X-Tapis-Token': `${authenticatedUser.authToken?.token}`,
});
});
it('returns no auth-related headers for unauthenticatedUser', () => {
- const headers = getHeaders(
- ApiService.Geoapi,
- localDevConfiguration,
- unauthenticatedUser
- );
+ const headers = getHeaders(ApiService.Geoapi, unauthenticatedUser);
expect(headers).toEqual({});
});
});
diff --git a/react/src/requests.ts b/react/src/requests.ts
index df9b90d7..7e1b9be7 100644
--- a/react/src/requests.ts
+++ b/react/src/requests.ts
@@ -8,13 +8,11 @@ import {
UseMutationOptions,
QueryKey,
} from 'react-query';
-import { useAppConfiguration } from './hooks';
import {
- ApiService,
- AppConfiguration,
- AuthState,
- GeoapiBackendEnvironment,
-} from './types';
+ useAppConfiguration,
+ useEnsureAuthenticatedUserHasValidTapisToken,
+} from './hooks';
+import { ApiService, AppConfiguration, AuthState } from './types';
import { v4 as uuidv4 } from 'uuid';
function getBaseApiUrl(
@@ -25,32 +23,32 @@ function getBaseApiUrl(
case ApiService.Geoapi:
return configuration.geoapiUrl;
case ApiService.DesignSafe:
- return configuration.designSafeUrl;
+ return configuration.designsafePortalUrl;
case ApiService.Tapis:
- // Tapis and DesignSafe are currently the same
- return configuration.designSafeUrl;
+ return 'https://designsafe.tapis.io';
default:
throw new Error('Unsupported api service Type.');
}
}
-export function getHeaders(
- apiService: ApiService,
- configuration: AppConfiguration,
- auth: AuthState
-) {
- // TODO_REACT add mapillary support
- if (auth.authToken?.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.authToken?.token}` };
+function usesTapisToken(apiService: ApiService) {
+ const servicesUsingTapisToken = [
+ ApiService.Geoapi,
+ ApiService.Tapis,
+ ApiService.DesignSafe,
+ ];
+ return servicesUsingTapisToken.includes(apiService);
+}
+
+export function getHeaders(apiService: ApiService, auth: AuthState) {
+ const hasTapisAuthToken = !!auth.authToken?.token;
+
+ if (hasTapisAuthToken && usesTapisToken(apiService)) {
+ return { 'X-Tapis-Token': auth.authToken?.token };
}
+
+ // TODO_REACT add mapillary support
+
return {};
}
@@ -81,8 +79,11 @@ export function useGet({
const client = axios;
const state = store.getState();
const configuration = useAppConfiguration();
+
+ useEnsureAuthenticatedUserHasValidTapisToken();
+
const baseUrl = getBaseApiUrl(apiService, configuration);
- const headers = getHeaders(apiService, configuration, state.auth);
+ const headers = getHeaders(apiService, state.auth);
let url = `${baseUrl}${endpoint}`;
@@ -137,8 +138,11 @@ export function usePost({
const state = store.getState();
const configuration = useAppConfiguration();
+ useEnsureAuthenticatedUserHasValidTapisToken();
+
const baseUrl = getBaseApiUrl(apiService, configuration);
- const headers = getHeaders(apiService, configuration, state.auth);
+
+ const headers = getHeaders(apiService, state.auth);
const postUtil = async (requestData: RequestType) => {
const response = await client.post(
diff --git a/react/src/secret_local.example.ts b/react/src/secret_local.example.ts
index 396ba952..ae7e1816 100644
--- a/react/src/secret_local.example.ts
+++ b/react/src/secret_local.example.ts
@@ -1,9 +1,5 @@
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/auth.ts b/react/src/types/auth.ts
index 5d0207e1..c78ba839 100644
--- a/react/src/types/auth.ts
+++ b/react/src/types/auth.ts
@@ -1,11 +1,10 @@
export interface AuthenticatedUser {
username: string;
- email: string;
}
export interface AuthToken {
- token: string | null;
- expires: number | null;
+ token: string;
+ expiresAt: string;
}
export interface AuthState {
diff --git a/react/src/types/environment.ts b/react/src/types/environment.ts
index d1ae074a..6e398eff 100644
--- a/react/src/types/environment.ts
+++ b/react/src/types/environment.ts
@@ -5,6 +5,7 @@ export enum GeoapiBackendEnvironment {
Production = 'production',
Staging = 'staging',
Dev = 'dev',
+ Experimental = 'experimental',
Local = 'local',
}
@@ -12,8 +13,10 @@ export enum GeoapiBackendEnvironment {
* 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 */,
+ Production = 'production' /* https://www.designsafe-ci.org/ */,
+ Dev = 'dev' /* https://designsafeci-dev.tacc.utexas.edu/ This dev is comparable to Geoapi's staging */,
+ Next = 'experimental' /* https://designsafeci-next.tacc.utexas.edu/ */,
+ Local = 'local' /* not supported but would be designsafe.dev */,
}
/**
@@ -23,7 +26,7 @@ export enum ApiService {
/* Geoapi api */
Geoapi = 'geoapi',
- /* DesignSafe api - for project listings */
+ /* DesignSafe api - for project listings + project metadata read/update */
DesignSafe = 'designsafe',
/* Tapis api - for system listings and file operations */
@@ -40,9 +43,6 @@ export enum ApiService {
*
*/
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;
}
@@ -69,19 +69,13 @@ 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. */
+ /** URL for the DesignSafe portal and API. */
designsafePortalUrl: string;
/** Mapillary related configuration */
@@ -89,7 +83,4 @@ export interface AppConfiguration {
/** URL for taggit */
taggitUrl: string;
-
- /** Optional JWT token used for development with local geoapi service. */
- jwt?: string;
}
diff --git a/react/src/utils/authUtils.test.ts b/react/src/utils/authUtils.test.ts
new file mode 100644
index 00000000..0a15eaaf
--- /dev/null
+++ b/react/src/utils/authUtils.test.ts
@@ -0,0 +1,89 @@
+import {
+ isTokenValid,
+ getAuthenticatedUserFromLocalStorage,
+ setAuthenticatedUserFromLocalStorage,
+ removeAuthenticatedUserFromLocalStorage,
+ AUTH_KEY,
+} from './authUtils';
+import { AuthState, AuthToken } from '../types';
+
+describe('Auth Utils', () => {
+ describe('isTokenValid', () => {
+ it('should return false if authToken is null', () => {
+ expect(isTokenValid(null)).toBe(false);
+ });
+
+ it('should return false if token is expired', () => {
+ const authToken: AuthToken = {
+ token: 'fakeToken',
+ expiresAt: new Date(Date.now() - 1000).toISOString(), // Expired 1 second ago
+ };
+ expect(isTokenValid(authToken)).toBe(false);
+ });
+
+ it('should return true if token is valid', () => {
+ const authToken: AuthToken = {
+ token: 'fakeToken',
+ expiresAt: new Date(Date.now() + 1000 * 60 * 10).toISOString(), // Expires in 10 minutes
+ };
+ expect(isTokenValid(authToken)).toBe(true);
+ });
+ });
+
+ describe('getAuthenticatedUserFromLocalStorage', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ });
+
+ it('should return null user and token if localStorage is empty', () => {
+ const result = getAuthenticatedUserFromLocalStorage();
+ expect(result).toEqual({ user: null, authToken: null });
+ });
+
+ it('should return the user and token from localStorage', () => {
+ const authState: AuthState = {
+ user: { username: 'testUser' },
+ authToken: { token: 'fakeToken', expiresAt: '2024-12-31T23:59:59Z' },
+ };
+ localStorage.setItem(AUTH_KEY, JSON.stringify(authState));
+
+ const result = getAuthenticatedUserFromLocalStorage();
+ expect(result).toEqual(authState);
+ });
+ });
+
+ describe('setAuthenticatedUserFromLocalStorage', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ });
+
+ it('should store the user and token in localStorage', () => {
+ const authState: AuthState = {
+ user: { username: 'testUser' },
+ authToken: { token: 'fakeToken', expiresAt: '2024-12-31T23:59:59Z' },
+ };
+
+ setAuthenticatedUserFromLocalStorage(authState);
+ const storedValue = localStorage.getItem(AUTH_KEY);
+ expect(storedValue).toEqual(JSON.stringify(authState));
+ });
+ });
+
+ describe('removeAuthenticatedUserFromLocalStorage', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ });
+
+ it('should remove the user and token from localStorage', () => {
+ const authState: AuthState = {
+ user: { username: 'testUser' },
+ authToken: { token: 'fakeToken', expiresAt: '2024-12-31T23:59:59Z' },
+ };
+ localStorage.setItem(AUTH_KEY, JSON.stringify(authState));
+
+ removeAuthenticatedUserFromLocalStorage();
+ const storedValue = localStorage.getItem(AUTH_KEY);
+ expect(storedValue).toBeNull();
+ });
+ });
+});
diff --git a/react/src/utils/authUtils.ts b/react/src/utils/authUtils.ts
index 0432cb6e..3a8c9146 100644
--- a/react/src/utils/authUtils.ts
+++ b/react/src/utils/authUtils.ts
@@ -1,37 +1,43 @@
-import { AuthToken } from '../types';
+import { AuthToken, AuthState } from '../types';
-export const AUTH_KEY = 'auth';
+export const AUTH_KEY = 'authV3';
export function isTokenValid(authToken: AuthToken | null): boolean {
if (authToken) {
- if (!authToken.expires) {
+ if (!authToken.expiresAt) {
return false;
}
- const now = Date.now();
- return now < authToken.expires;
+ const now = new Date();
+ const expiresAtDate: Date = new Date(authToken.expiresAt);
+ return now < expiresAtDate;
} else {
return false;
}
}
-export function getTokenFromLocalStorage(): AuthToken {
+/**
+ * Retrieves the authentication information (user, token etc) from local storage.
+ *
+ * If not found in local storage, the function returns `null`.
+ */
+export function getAuthenticatedUserFromLocalStorage(): AuthState {
try {
- const tokenStr = localStorage.getItem(AUTH_KEY);
- if (tokenStr) {
- const auth = JSON.parse(tokenStr);
- return auth;
+ const authenticatedUserJson = localStorage.getItem(AUTH_KEY);
+ if (authenticatedUserJson) {
+ const authState = JSON.parse(authenticatedUserJson);
+ return authState;
}
} catch (e) {
console.error('Error loading state from localStorage:', e);
}
- return { token: null, expires: null };
+ return { user: null, authToken: null };
}
-export function setTokenToLocalStorage(authToken: AuthToken) {
+export function setAuthenticatedUserFromLocalStorage(authToken: AuthState) {
localStorage.setItem(AUTH_KEY, JSON.stringify(authToken));
}
-export function removeTokenFromLocalStorage() {
+export function removeAuthenticatedUserFromLocalStorage() {
localStorage.removeItem(AUTH_KEY);
}
|