diff --git a/package-lock.json b/package-lock.json
index 93bc0e7f5..7a2de2d93 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "@bcgsc-pori/graphkb-client",
- "version": "4.2.3",
+ "version": "4.2.4",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -3625,11 +3625,6 @@
}
}
},
- "@scarf/scarf": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.0.5.tgz",
- "integrity": "sha512-9WKaGVpQH905Aqkk+BczFEeLQxS07rl04afFRPUG9IcSlOwmo5EVVuuNu0d4M9LMYucObvK0LoAe+5HfMW2QhQ=="
- },
"@sheerun/mutationobserver-shim": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz",
@@ -5506,8 +5501,7 @@
"base64-js": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
- "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==",
- "dev": true
+ "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
},
"batch": {
"version": "0.6.1",
@@ -13181,6 +13175,11 @@
"integrity": "sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ==",
"dev": true
},
+ "js-sha256": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
+ "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA=="
+ },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -13404,9 +13403,13 @@
}
},
"keycloak-js": {
- "version": "4.8.3",
- "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-4.8.3.tgz",
- "integrity": "sha512-TXoZdoOYu2ScYs58L95/xSYjsTto9KRvZ+vt6mv4Dyf4pYhYZSgwMPnmi128qj/z8sm4mL1Z8nncR6XdWgNKMQ=="
+ "version": "12.0.4",
+ "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-12.0.4.tgz",
+ "integrity": "sha512-O/BHtyiDrZrUnKBrVF8POojqd3gmhuiDw4FiI+FbnB14nu7G5jKFrKYZa9Q0JYKIZXHJOBzSaKQcMp2WUI+zmA==",
+ "requires": {
+ "base64-js": "1.3.1",
+ "js-sha256": "0.9.0"
+ }
},
"killable": {
"version": "1.0.1",
@@ -14132,12 +14135,22 @@
"dev": true
},
"mini-create-react-context": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz",
- "integrity": "sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA==",
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
+ "integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
"requires": {
- "@babel/runtime": "^7.5.5",
+ "@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.16.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz",
+ "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==",
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ }
}
},
"mini-css-extract-plugin": {
@@ -16003,6 +16016,7 @@
"run-async": "^2.2.0",
"rx-lite": "^4.0.8",
"rx-lite-aggregates": "^4.0.8",
+ "string-width": "^2.1.0",
"strip-ansi": "^4.0.0",
"through": "^2.3.6"
},
@@ -16063,6 +16077,33 @@
"yallist": "^2.1.2"
}
},
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dev": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ }
+ }
+ },
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
@@ -16135,20 +16176,19 @@
"integrity": "sha512-aRGxDGP9VoLxcsaYvKWIW+LRrMOzz2eEcubTS4NvQPPugjk2VvMhow0wWTkSl7RxookomD1MwcP4l5UStg5ShQ=="
},
"react-query": {
- "version": "1.5.7",
- "resolved": "https://registry.npmjs.org/react-query/-/react-query-1.5.7.tgz",
- "integrity": "sha512-VyX3CwfRtEIQN5Y34cOX8cIx6pKsJMKlFrfCtGfbBDLbmqgMQctc9i84W7FjI+eXcDQ/BMIASccyp5D88N9znw==",
+ "version": "2.26.4",
+ "resolved": "https://registry.npmjs.org/react-query/-/react-query-2.26.4.tgz",
+ "integrity": "sha512-sXGG0gh1ah11AcfptYOCRpGDoYMnssq6riQUpQaLSM2EOodVkexp3zNLk1MFDgfRGuXQst40Tnu17oNwni66aA==",
"requires": {
- "@scarf/scarf": "^1.0.0",
- "ts-toolbelt": "^6.4.2"
+ "@babel/runtime": "^7.5.5"
}
},
"react-router": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
- "integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
+ "integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
"requires": {
- "@babel/runtime": "^7.1.2",
+ "@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
@@ -16158,20 +16198,40 @@
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.16.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz",
+ "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==",
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ }
}
},
"react-router-dom": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz",
- "integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
+ "integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
"requires": {
- "@babel/runtime": "^7.1.2",
+ "@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
- "react-router": "5.2.0",
+ "react-router": "5.2.1",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.16.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz",
+ "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==",
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ }
}
},
"react-select": {
@@ -18357,7 +18417,8 @@
},
"ssri": {
"version": "6.0.1",
- "resolved": "",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
+ "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
"dev": true,
"requires": {
"figgy-pudding": "^3.5.1"
@@ -19305,9 +19366,9 @@
}
},
"tiny-invariant": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
- "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
+ "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg=="
},
"tiny-warning": {
"version": "1.0.3",
@@ -19450,11 +19511,6 @@
"integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==",
"dev": true
},
- "ts-toolbelt": {
- "version": "6.9.4",
- "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.9.4.tgz",
- "integrity": "sha512-muRZZqfOTOVvLk5cdnp7YWm6xX+kD/WL2cS/L4zximBRcbQSuMoTbQQ2ZZBVMs1gB0EZw1qThP+HrIQB35OmEw=="
- },
"tslib": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
diff --git a/package.json b/package.json
index 50441bc27..4b27072cd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@bcgsc-pori/graphkb-client",
- "version": "4.2.3",
+ "version": "4.2.4",
"private": true,
"bugs": {
"email": "graphkb@bcgsc.ca"
@@ -35,7 +35,7 @@
"json-cycle": "^1.3.0",
"jsonwebtoken": "^8.3.0",
"jss": "^10.1.1",
- "keycloak-js": "^4.8.2",
+ "keycloak-js": "~12.0.4",
"lodash.isobject": "^3.0.2",
"lodash.merge": "^4.6.2",
"lodash.omit": "^4.5.0",
@@ -47,8 +47,8 @@
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-google-charts": "^3.0.15",
- "react-query": "^1.5.7",
- "react-router-dom": "^5.2.0",
+ "react-query": "^2.26.4",
+ "react-router-dom": "~5.3.0",
"react-select": "^2.4.4",
"slugify": "^1.4.0",
"use-debounce": "^3.4.2",
diff --git a/src/App.js b/src/App.js
index 2c17e9d47..3ce9cc3c8 100644
--- a/src/App.js
+++ b/src/App.js
@@ -13,6 +13,8 @@ import { SnackbarProvider } from 'notistack';
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
+import { AuthProvider } from '@/components/Auth';
+
import * as cssTheme from './_theme.scss';
import MainView from './views/MainView';
@@ -78,7 +80,9 @@ function App() {
-
+
+
+
diff --git a/src/components/Auth/index.js b/src/components/Auth/index.js
new file mode 100644
index 000000000..97f861ab9
--- /dev/null
+++ b/src/components/Auth/index.js
@@ -0,0 +1,205 @@
+import './index.scss';
+
+import { Button, CircularProgress, Typography } from '@material-ui/core';
+import fetchIntercept from 'fetch-intercept';
+import * as jwt from 'jsonwebtoken';
+import Keycloak from 'keycloak-js';
+import { PropTypes } from 'prop-types';
+import React, {
+ createContext, useContext, useEffect, useLayoutEffect, useMemo,
+} from 'react';
+import { useMutation } from 'react-query';
+import { Route } from 'react-router-dom';
+
+import api from '@/services/api';
+
+const dbRoles = {
+ admin: 'admin',
+ regular: 'regular',
+ readonly: 'readonly',
+};
+
+const keycloak = Keycloak({
+ realm: window._env_.KEYCLOAK_REALM,
+ clientId: window._env_.KEYCLOAK_CLIENT_ID,
+ url: window._env_.KEYCLOAK_URL,
+ realm_access: { roles: [window._env_.KEYCLOAK_ROLE] },
+});
+
+const AuthContext = createContext(undefined);
+
+const useAuth = () => {
+ const state = useContext(AuthContext);
+
+ if (!state) {
+ throw new Error('context provider for AuthContext is missing');
+ }
+
+ return state;
+};
+
+const AuthProvider = (props) => {
+ const { children } = props;
+
+ const [logInOrOut, { isLoading: isAuthenticating, data, error }] = useMutation(
+ async ({ loggingIn }) => {
+ if (loggingIn) {
+ const loggedIn = await keycloak.init({
+ checkLoginIframe: false,
+ enableLogging: true,
+ onLoad: 'login-required',
+ });
+
+ if (!loggedIn) {
+ await keycloak.login({ redirectUri: window.location.href });
+ }
+
+ const { kbToken: authorizationToken } = await api.post('/token', { keyCloakToken: keycloak.token }).request();
+ const { user } = jwt.decode(authorizationToken);
+
+ await keycloak.loadUserInfo();
+ // eslint-disable-next-line camelcase
+ const username = keycloak.userInfo?.preferred_username || user?.name;
+
+ return {
+ authenticationToken: keycloak.token,
+ authorizationToken,
+ isAuthenticated: true,
+ isAdmin: Boolean(user.groups.find(group => group.name === dbRoles.admin)),
+ hasWriteAccess: Boolean(user.groups.find(group => [dbRoles.admin, dbRoles.regular].includes(group.name))),
+ user,
+ username,
+ };
+ }
+
+ await keycloak.logout();
+ return undefined;
+ },
+ );
+
+ const { authorizationToken } = data ?? {};
+
+ useEffect(() => {
+ const unregister = fetchIntercept.register({
+ request: (fetchUrl, fetchConfig) => {
+ if (fetchUrl.startsWith(window._env_.API_BASE_URL)) {
+ const newConfig = { ...fetchConfig };
+
+ if (!newConfig.headers) {
+ newConfig.headers = {};
+ }
+ newConfig.headers.Authorization = authorizationToken;
+ return [fetchUrl, newConfig];
+ }
+ return [fetchUrl, fetchConfig];
+ },
+ });
+ return unregister;
+ }, [authorizationToken]);
+
+ const auth = useMemo(() => ({
+ login: () => {
+ if (!isAuthenticating) {
+ logInOrOut({ loggingIn: true });
+ }
+ },
+ logout: () => {
+ if (!isAuthenticating) {
+ logInOrOut({ loggingIn: false });
+ }
+ },
+ isAuthenticating,
+ error,
+ ...data || {},
+ }), [data, isAuthenticating, logInOrOut, error]);
+
+ return (
+
+ {children}
+
+ );
+};
+AuthProvider.propTypes = {
+ children: PropTypes.node.isRequired,
+};
+
+const Centered = ({ children }) => (
+
+ {children}
+
+);
+Centered.propTypes = {
+ children: PropTypes.node.isRequired,
+};
+
+const AuthenticatedRoute = (props) => {
+ const { admin, component, path } = props;
+ const auth = useAuth();
+
+ useLayoutEffect(() => {
+ if (!auth.isAuthenticating && !auth.isAuthenticated && !auth.error) {
+ auth.login();
+ }
+ }, [auth]);
+
+ if (auth.error) {
+ return (
+
+
+ Error Authenticating
+ An Error occurred while authenticating. please logout and try again or contact your administrator if the problem persists
+ {auth.error?.message}
+
+
+
+ );
+ }
+
+ if (!auth.isAuthenticated) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (admin && !auth.isAdmin) {
+ return (
+
+
+ Forbidden
+ You do not have sufficient permissions to see this page.
+
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+
+AuthenticatedRoute.propTypes = {
+ component: PropTypes.object.isRequired,
+ path: PropTypes.string.isRequired,
+ admin: PropTypes.bool,
+};
+
+AuthenticatedRoute.defaultProps = {
+ admin: false,
+};
+
+export {
+ AuthProvider, useAuth, AuthenticatedRoute, AuthContext,
+};
diff --git a/src/components/Auth/index.scss b/src/components/Auth/index.scss
new file mode 100644
index 000000000..1a135e04c
--- /dev/null
+++ b/src/components/Auth/index.scss
@@ -0,0 +1,7 @@
+.auth-centered {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ padding-top: 40px;
+ width: 100%;
+}
\ No newline at end of file
diff --git a/src/components/AuthenticatedRoute.js b/src/components/AuthenticatedRoute.js
deleted file mode 100644
index fff0de555..000000000
--- a/src/components/AuthenticatedRoute.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import { PropTypes } from 'prop-types';
-import React, { useContext, useEffect } from 'react';
-import {
- Redirect,
- Route,
-} from 'react-router-dom';
-
-import ActiveLinkContext from '@/components/ActiveLinkContext';
-import { SecurityContext } from '@/components/SecurityContext';
-import { LocationPropType } from '@/components/types';
-import { isAdmin, isAuthenticated } from '@/services/auth';
-
-/**
- * @returns {Route} a route component which checks authentication on render or redirects to login
- */
-const AuthenticatedRoute = ({
- component: Component, admin, path, ...rest
-}) => {
- const {
- autheticationToken, authorizationToken,
- } = useContext(SecurityContext);
- const { activeLink, setActiveLink } = useContext(ActiveLinkContext);
-
- useEffect(() => {
- if (path !== activeLink) {
- setActiveLink(path);
- }
- }, [activeLink, path, setActiveLink]);
-
- const authOk = isAuthenticated({ autheticationToken });
- const adminOk = isAdmin({ autheticationToken, authorizationToken });
-
- let ChildComponent;
-
- if (!authOk) {
- ChildComponent = (props) => {
- const { location } = props;
- return (
-
- );
- };
- } else if (admin && !adminOk) {
- setActiveLink('/');
- ChildComponent = () => (
-
- );
- } else {
- ChildComponent = Component;
- }
- return (
- ()}
- />
- );
-};
-
-AuthenticatedRoute.propTypes = {
- component: PropTypes.object.isRequired,
- location: LocationPropType.isRequired,
- path: PropTypes.string.isRequired,
- admin: PropTypes.bool,
-};
-
-AuthenticatedRoute.defaultProps = {
- admin: false,
-};
-
-export default AuthenticatedRoute;
diff --git a/src/components/DetailDrawer/__tests__/index.test.js b/src/components/DetailDrawer/__tests__/index.test.js
index 715f0fe26..3c6c789f1 100644
--- a/src/components/DetailDrawer/__tests__/index.test.js
+++ b/src/components/DetailDrawer/__tests__/index.test.js
@@ -8,7 +8,7 @@ import {
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
-import { SecurityContext } from '@/components/SecurityContext';
+import { AuthContext } from '@/components/Auth';
import DetailDrawer from '..';
@@ -85,9 +85,9 @@ const statementNode = {
const ProvideSchema = ({ children = [], schema }) => ( // eslint-disable-line
-
+
{children}
-
+
);
diff --git a/src/components/DetailDrawer/index.js b/src/components/DetailDrawer/index.js
index 377f353df..6e04d23ab 100644
--- a/src/components/DetailDrawer/index.js
+++ b/src/components/DetailDrawer/index.js
@@ -20,17 +20,14 @@ import ExpandLessIcon from '@material-ui/icons/ExpandLess';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
import PropTypes from 'prop-types';
-import React, {
- useContext, useEffect, useState,
-} from 'react';
+import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
-import { SecurityContext } from '@/components/SecurityContext';
import { GeneralRecordPropType } from '@/components/types';
-import { hasWriteAccess } from '@/services/auth';
import schema from '@/services/schema';
import util from '@/services/util';
+import { useAuth } from '../Auth';
import LinkEmbeddedPropList from './LinkEmbeddedPropList';
import RelationshipList from './RelationshipList';
import SetPropsList from './SetPropsList';
@@ -71,7 +68,7 @@ function DetailDrawer(props) {
isEdge,
} = props;
- const context = useContext(SecurityContext);
+ const auth = useAuth();
const [opened, setOpened] = useState([]);
@@ -238,7 +235,7 @@ function DetailDrawer(props) {
)}
- {hasWriteAccess(context) && (
+ {auth.hasWriteAccess && (
({
- getUser: () => '23:9',
-}));
+const auth = { user: { '@rid': '23:9' } };
jest.mock('@/services/api', () => {
const mockRequest = () => ({
@@ -84,7 +82,7 @@ describe('RecordForm', () => {
beforeEach(() => {
({ getByText, queryByText, getByTestId } = render(
-
+
{
variant="view"
/>
- ,
+ ,
));
});
@@ -134,7 +132,7 @@ describe('RecordForm', () => {
beforeEach(() => {
({ getByText, getByTestId } = render(
-
+
{
variant="edit"
/>
- ,
+ ,
));
});
@@ -184,7 +182,7 @@ describe('RecordForm', () => {
beforeEach(() => {
({ getByText, getByTestId } = render(
-
+
{
value={{ }}
variant="new"
/>
-
+
,
));
});
diff --git a/src/components/SecurityContext/__tests__/index.js b/src/components/SecurityContext/__tests__/index.js
deleted file mode 100644
index 22e1d388e..000000000
--- a/src/components/SecurityContext/__tests__/index.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { mount } from 'enzyme';
-import React from 'react';
-
-import { SecurityContext, withKB } from '..';
-
-describe('KB Context provider and consumers', () => {
- test('consumer inherits value', () => {
- const Div = withKB((props) => {
- const { authorizationToken } = props;
- return (
-
- );
- });
-
- const wrapper = mount(
-
-
- ,
- );
-
- expect(wrapper.find('#test-div').props().value).toBe('test');
- });
-});
diff --git a/src/components/SecurityContext/index.js b/src/components/SecurityContext/index.js
deleted file mode 100644
index df8e7fea9..000000000
--- a/src/components/SecurityContext/index.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import React from 'react';
-
-
-/**
- * Passes user values to wrapped consumers.
- */
-const SecurityContext = React.createContext({
- authorizationToken: '',
- authenticationToken: '',
- setAuthenticationToken: () => {},
- setAuthorizationToken: () => {},
-});
-
-const withKB = Child => props => (
-
- {kbValues => (
-
- )}
-
-);
-
-export {
- SecurityContext,
- withKB,
-};
-
-export default SecurityContext;
diff --git a/src/components/StatementForm/ReviewDialog.js b/src/components/StatementForm/ReviewDialog.js
index a464c6fd6..4007fcc79 100644
--- a/src/components/StatementForm/ReviewDialog.js
+++ b/src/components/StatementForm/ReviewDialog.js
@@ -7,19 +7,16 @@ import {
import CancelIcon from '@material-ui/icons/Cancel';
import { useSnackbar } from 'notistack';
import PropTypes from 'prop-types';
-import React, {
- useCallback, useContext, useState,
-} from 'react';
+import React, { useCallback, useState } from 'react';
import ActionButton from '@/components/ActionButton';
+import { useAuth } from '@/components/Auth';
import FormContext from '@/components/FormContext';
import FormField from '@/components/FormField';
import useSchemaForm from '@/components/hooks/useSchemaForm';
-import { SecurityContext } from '@/components/SecurityContext';
import {
FORM_VARIANT,
} from '@/components/util';
-import { getUser } from '@/services/auth';
import schema from '@/services/schema';
@@ -34,7 +31,7 @@ const AddReviewDialog = ({
onSubmit, isOpen, onClose,
}) => {
const snackbar = useSnackbar();
- const context = useContext(SecurityContext);
+ const auth = useAuth();
const { comment, status } = schema.get(MODEL_NAME).properties;
const [updateAmalgamated, setUpdateAmalgamated] = useState(true);
@@ -57,10 +54,10 @@ const AddReviewDialog = ({
console.error(formErrors);
snackbar.enqueueSnackbar('There are errors in the form which must be resolved before it can be submitted', { variant: 'error' });
} else {
- const content = { ...formContent, '@class': MODEL_NAME, createdBy: getUser(context) };
+ const content = { ...formContent, '@class': MODEL_NAME, createdBy: auth.user };
onSubmit(content, updateAmalgamated);
}
- }, [context, formContent, formErrors, formHasErrors, onSubmit, setFormIsDirty, snackbar, updateAmalgamated]);
+ }, [auth, formContent, formErrors, formHasErrors, onSubmit, setFormIsDirty, snackbar, updateAmalgamated]);
return (
diff --git a/src/components/StatementForm/__tests__/ReviewDialog.formActions.test.js b/src/components/StatementForm/__tests__/ReviewDialog.formActions.test.js
index a5944dd8d..ee4fef54c 100644
--- a/src/components/StatementForm/__tests__/ReviewDialog.formActions.test.js
+++ b/src/components/StatementForm/__tests__/ReviewDialog.formActions.test.js
@@ -4,15 +4,11 @@ import { act, fireEvent, render } from '@testing-library/react';
import { SnackbarProvider } from 'notistack';
import React from 'react';
-import { SecurityContext } from '@/components/SecurityContext';
+import { AuthContext } from '@/components/Auth';
import ReviewDialog from '../ReviewDialog';
-jest.mock('@/services/auth', () => ({
- getUser: () => ({ '@rid': '#20:0' }),
-}));
-
/* eslint-disable react/prop-types */
jest.mock('../../DropDownSelect', () => ({
options = [], value, onChange, name,
@@ -50,8 +46,9 @@ describe('ReviewDialog formActions', () => {
});
beforeEach(() => {
+ const auth = { user: { '@rid': '#20:0' } };
({ getByText, getByTestId } = render(
-
+
{
onSubmit={onSubmitSpy}
/>
- ,
+ ,
));
});
diff --git a/src/components/StatementForm/__tests__/ReviewDialog.test.js b/src/components/StatementForm/__tests__/ReviewDialog.test.js
index f539470bb..a7eccf7c5 100644
--- a/src/components/StatementForm/__tests__/ReviewDialog.test.js
+++ b/src/components/StatementForm/__tests__/ReviewDialog.test.js
@@ -4,16 +4,11 @@ import { render } from '@testing-library/react';
import { SnackbarProvider } from 'notistack';
import React from 'react';
-import { SecurityContext } from '@/components/SecurityContext';
+import { AuthContext } from '@/components/Auth';
import ReviewDialog from '../ReviewDialog';
-jest.mock('@/services/auth', () => ({
- getUser: () => ({ '@rid': '#20:0' }),
-}));
-
-
describe('ReviewDialog', () => {
let getByText;
let queryByText;
@@ -28,8 +23,9 @@ describe('ReviewDialog', () => {
});
beforeEach(() => {
+ const auth = { user: { '@rid': '#20:0' } };
({ getByText, queryByText, getAllByText } = render(
-
+
{
onSubmit={onSubmitSpy}
/>
- ,
+ ,
));
});
diff --git a/src/components/StatementForm/__tests__/index.test.js b/src/components/StatementForm/__tests__/index.test.js
index ef4858982..a1ac8dcbc 100644
--- a/src/components/StatementForm/__tests__/index.test.js
+++ b/src/components/StatementForm/__tests__/index.test.js
@@ -4,13 +4,12 @@ import { fireEvent, render } from '@testing-library/react';
import { SnackbarProvider } from 'notistack';
import React from 'react';
-import { SecurityContext } from '@/components/SecurityContext';
+import { AuthContext } from '@/components/Auth';
import StatementForm from '..';
-jest.mock('@/services/auth', () => ({
- getUser: () => '23:9',
-}));
+
+const auth = { user: { '@rid': '23:9' } };
jest.mock('@/services/api', () => {
const mockRequest = () => ({
@@ -77,7 +76,7 @@ describe('StatementForm', () => {
test('edit statement shows add review for statements', () => {
const { getByText } = render(
-
+
{
variant="edit"
/>
- ,
+ ,
);
expect(getByText('Add Review')).toBeInTheDocument();
});
@@ -101,7 +100,7 @@ describe('StatementForm', () => {
beforeEach(() => {
({ getByText, getByTestId } = render(
-
+
{
value={{ }}
variant="new"
/>
-
+
,
));
});
diff --git a/src/components/StatementForm/index.js b/src/components/StatementForm/index.js
index a66784850..e9e8dfca3 100644
--- a/src/components/StatementForm/index.js
+++ b/src/components/StatementForm/index.js
@@ -10,21 +10,19 @@ import { Alert } from '@material-ui/lab';
import { useSnackbar } from 'notistack';
import PropTypes from 'prop-types';
import React, {
- useCallback, useContext, useEffect, useRef,
- useState,
+ useCallback, useEffect, useRef, useState,
} from 'react';
import { useQuery } from 'react-query';
import ActionButton from '@/components/ActionButton';
+import { useAuth } from '@/components/Auth';
import FormContext from '@/components/FormContext';
import FormLayout from '@/components/FormLayout';
import useSchemaForm from '@/components/hooks/useSchemaForm';
import RecordFormStateToggle from '@/components/RecordFormStateToggle';
-import { SecurityContext } from '@/components/SecurityContext';
import { GeneralRecordPropType } from '@/components/types';
import { cleanPayload, FORM_VARIANT } from '@/components/util';
import api from '@/services/api';
-import { getUser } from '@/services/auth';
import schema from '@/services/schema';
import CivicEvidenceLink from './CivicEvidenceLink';
@@ -84,7 +82,7 @@ const StatementForm = ({
}], async (route, body) => api.post(route, body).request());
const snackbar = useSnackbar();
- const context = useContext(SecurityContext);
+ const auth = useAuth();
const model = schemaDefn.schema.Statement;
const fieldDefs = model.properties;
@@ -159,7 +157,7 @@ const StatementForm = ({
}
if (!currContent.reviews) {
- const createdBy = getUser(context);
+ const createdBy = auth.user;
updatedContent.reviews = [{
status: 'initial',
comment: '',
@@ -168,7 +166,7 @@ const StatementForm = ({
}
return updatedContent;
- }, [context]);
+ }, [auth]);
/**
* Handler for submission of a new record
diff --git a/src/services/__tests__/auth.test.js b/src/services/__tests__/auth.test.js
deleted file mode 100644
index f0ccd07cb..000000000
--- a/src/services/__tests__/auth.test.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import jwt from 'jsonwebtoken';
-
-import {
- getUser, getUsername, isAdmin,
- isAuthenticated, isAuthorized,
-} from '../auth';
-
-const TEST_USER = { name: 'test user', groups: [{ name: 'not admin' }] };
-const ADMIN_USER = { name: 'admin user', groups: [{ name: 'admin' }] };
-const REALLY_LONG_TIME = 1000000000000;
-const ENCRYPTION_KEY = 'NotSuperSecret';
-
-describe('auth methods test', () => {
- const EXPIRED_JWT = jwt.sign({ user: ADMIN_USER }, ENCRYPTION_KEY, { expiresIn: 0 });
- const VALID_JWT = jwt.sign({ user: TEST_USER, preferred_username: 'keycloak username' }, ENCRYPTION_KEY, { expiresIn: REALLY_LONG_TIME });
- const ADMIN_JWT = jwt.sign({ user: ADMIN_USER }, ENCRYPTION_KEY, { expiresIn: REALLY_LONG_TIME });
-
- describe('expired token', () => {
- test('retrieved the user', () => {
- const user = getUser({ authorizationToken: EXPIRED_JWT });
- expect(user).toEqual(ADMIN_USER);
- });
-
- test('is not authenticated', () => {
- expect(isAuthenticated({ authorizationToken: EXPIRED_JWT, authenticationToken: EXPIRED_JWT })).toBe(false);
- });
-
- test('is not authorized', () => {
- expect(isAuthorized({ authorizationToken: EXPIRED_JWT, authenticationToken: EXPIRED_JWT })).toBe(false);
- });
- });
-
- describe('valid token', () => {
- test('retrieved the user', () => {
- const user = getUser({ authorizationToken: VALID_JWT });
- expect(user).toEqual(TEST_USER);
- });
-
- test('is authenticated', () => {
- expect(isAuthenticated({ authorizationToken: VALID_JWT, authenticationToken: VALID_JWT })).toBe(true);
- });
-
- test('is authorized', () => {
- expect(isAuthorized({ authorizationToken: VALID_JWT, authenticationToken: VALID_JWT })).toBe(true);
- });
-
- test('is not admin', () => {
- expect(isAdmin({ authorizationToken: VALID_JWT })).toBe(false);
- });
- });
-
- describe('getUsername', () => {
- test('get username from authorizationToken', () => {
- const name = getUsername({ authorizationToken: VALID_JWT, authenticationToken: VALID_JWT });
- expect(name).toEqual('test user');
- });
-
- test('falls back to authenticationToken if not authorizationToken', () => {
- const name = getUsername({ authenticationToken: VALID_JWT });
- expect(name).toEqual('keycloak username');
- });
- });
-
- describe('admin token', () => {
- test('retrieved the user', () => {
- const user = getUser({ authorizationToken: ADMIN_JWT });
- expect(user).toEqual(ADMIN_USER);
- });
-
- test('is authenticated', () => {
- expect(isAuthenticated({ authorizationToken: ADMIN_JWT, authenticationToken: ADMIN_JWT })).toBe(true);
- });
-
- test('is not authorized', () => {
- expect(isAuthenticated({ authorizationToken: ADMIN_JWT, authenticationToken: ADMIN_JWT })).toBe(true);
- });
-
- test('is admin', () => {
- expect(isAdmin({ authorizationToken: ADMIN_JWT, authenticationToken: ADMIN_JWT })).toBe(true);
- });
- });
-});
diff --git a/src/services/auth.js b/src/services/auth.js
deleted file mode 100644
index acf57614a..000000000
--- a/src/services/auth.js
+++ /dev/null
@@ -1,159 +0,0 @@
-/**
- * Handles token storage and authentication.
- * @module /services/auth
- */
-import * as jwt from 'jsonwebtoken';
-import Keycloak from 'keycloak-js';
-
-
-// must store the referring uri in local to get around the redirect
-const KEYCLOAK_REFERRER = 'KEYCLOAK_REFERRER';
-const dbRoles = {
- admin: 'admin',
- regular: 'regular',
- readonly: 'readonly',
-};
-
-const keycloak = Keycloak({
- realm: window._env_.KEYCLOAK_REALM,
- clientId: window._env_.KEYCLOAK_CLIENT_ID,
- url: window._env_.KEYCLOAK_URL,
- realm_access: { roles: [window._env_.KEYCLOAK_ROLE] },
-});
-
-/**
- * Checks expiry date on JWT token and compares with current time.
- */
-const isExpired = (token) => {
- try {
- const expiry = jwt.decode(token).exp;
- return !Number.isNaN(expiry) && (expiry * 1000) < (new Date()).getTime();
- } catch (err) {
- return false;
- }
-};
-
-/**
- * Checks that the token is formatted properly and can be decoded
- */
-const validToken = (token) => {
- try {
- const decoded = jwt.decode(token);
- return !!decoded;
- } catch (err) {
- return false;
- }
-};
-
-
-const getReferrerUri = () => localStorage.getItem(KEYCLOAK_REFERRER);
-
-const setReferrerUri = (uri) => {
- if (uri === null) {
- localStorage.removeItem(KEYCLOAK_REFERRER);
- } else {
- localStorage.setItem(KEYCLOAK_REFERRER, uri);
- }
-};
-
-/**
- * Primarily used for display when logged in
- */
-const getUsername = ({ authorizationToken, authenticationToken }) => {
- if (authorizationToken) {
- return jwt.decode(authorizationToken).user.name;
- } if (authenticationToken || keycloak.token) {
- return jwt.decode(authenticationToken || keycloak.token).preferred_username;
- }
- return null;
-};
-
-const getUser = ({ authorizationToken }) => {
- try {
- return jwt.decode(authorizationToken).user;
- } catch {
- return null;
- }
-};
-
-/**
- * Returns true if the user has been sucessfully authenticated and the token is valid
- */
-const isAuthenticated = ({ authenticationToken }) => {
- const token = authenticationToken || keycloak.token;
-
- if (token) {
- // check that the token is not expired
- return Boolean(validToken(token) && !isExpired(token));
- }
- return false;
-};
-
-const isAuthorized = ({ authorizationToken, authenticationToken }) => {
- if (isAuthenticated({ authenticationToken })) {
- return Boolean(validToken(authorizationToken) && !isExpired(authorizationToken));
- }
- return false;
-};
-
-/**
- * Returns true if user is in the 'admin' usergroup.
- */
-const isAdmin = ({ authorizationToken }) => {
- try {
- return Boolean(
- getUser({ authorizationToken }).groups.find(group => group.name === dbRoles.admin),
- );
- } catch (err) {
- return false;
- }
-};
-
-const hasWriteAccess = ({ authorizationToken }) => {
- try {
- return Boolean(
- getUser({ authorizationToken }).groups.find(group => [dbRoles.admin, dbRoles.regular].includes(group.name)),
- );
- } catch (err) {
- return false;
- }
-};
-
-const login = async (referrerUri = null) => {
- setReferrerUri(referrerUri);
- const init = new Promise((resolve, reject) => {
- const prom = keycloak.init({ onLoad: 'login-required' }); // setting promiseType = native does not work for later functions inside the closure
- prom.success(resolve);
- prom.error(reject);
- });
- await init;
-};
-
-const logout = async () => {
- setReferrerUri(null);
-
- try {
- const resp = await keycloak.logout({
- redirectUri: `${window.location.origin}${window._env_.PUBLIC_PATH}login`,
- });
- return resp;
- } catch (err) {
- return err;
- }
-};
-
-
-export {
- login,
- logout,
- hasWriteAccess,
- isAdmin,
- isAuthorized,
- isAuthenticated,
- isExpired,
- validToken,
- getUser,
- getUsername,
- getReferrerUri,
- keycloak,
-};
diff --git a/src/views/AboutView/components/AboutUsageTerms.js b/src/views/AboutView/components/AboutUsageTerms.js
index e1b13d0be..2526c9ed5 100644
--- a/src/views/AboutView/components/AboutUsageTerms.js
+++ b/src/views/AboutView/components/AboutUsageTerms.js
@@ -6,19 +6,17 @@ import {
import { formatDistanceToNow } from 'date-fns';
import { useSnackbar } from 'notistack';
import React, {
- useCallback,
- useContext, useEffect, useState,
+ useCallback, useEffect, useState,
} from 'react';
import ActionButton from '@/components/ActionButton';
-import { SecurityContext } from '@/components/SecurityContext';
+import { useAuth } from '@/components/Auth';
import api from '@/services/api';
-import { getUser } from '@/services/auth';
import TableOfContext from './TableOfContents';
const AboutUsageTerms = () => {
- const security = useContext(SecurityContext);
+ const auth = useAuth();
const snackbar = useSnackbar();
const [isSigned, setIsSigned] = useState(false);
@@ -39,15 +37,15 @@ const AboutUsageTerms = () => {
};
getData();
return () => controller && controller.abort();
- }, [security]);
+ }, [auth]);
useEffect(() => {
let controller;
const getData = async () => {
- const user = getUser(security);
+ const { user } = auth;
- if (!user.signedLicenseAt || user.signedLicenseAt < licenseEnactedAt) {
+ if (!user || !user.signedLicenseAt || user.signedLicenseAt < licenseEnactedAt) {
setRequiresSigning(true);
} else {
setIsSigned(true);
@@ -56,7 +54,7 @@ const AboutUsageTerms = () => {
};
getData();
return () => controller && controller.abort();
- }, [licenseEnactedAt, security]);
+ }, [licenseEnactedAt, auth]);
const handleConfirmSign = useCallback(async () => {
diff --git a/src/views/AboutView/components/Matching/index.js b/src/views/AboutView/components/Matching/index.js
index 8542fde5e..c2f828298 100644
--- a/src/views/AboutView/components/Matching/index.js
+++ b/src/views/AboutView/components/Matching/index.js
@@ -130,7 +130,8 @@ const MatchView = (props) => {
queries.map(async query => queryCache.prefetchQuery(
['/query', query],
async (key, bodykey) => api.post(key, bodykey).request(),
- { staleTime: Infinity, throwOnError: false },
+ { staleTime: Infinity },
+ { throwOnError: false },
)),
);
diff --git a/src/views/ErrorView/index.js b/src/views/ErrorView/index.js
index 4d1b83698..0be29a2f0 100644
--- a/src/views/ErrorView/index.js
+++ b/src/views/ErrorView/index.js
@@ -9,9 +9,7 @@ import AssignmentIcon from '@material-ui/icons/Assignment';
import { copy } from 'copy-to-clipboard';
import PropTypes from 'prop-types';
import React, { useCallback, useState } from 'react';
-import { Link } from 'react-router-dom';
-
-import { LocationPropType } from '@/components/types';
+import { Link, useLocation } from 'react-router-dom';
const EmailReportError = (props) => {
@@ -39,11 +37,10 @@ EmailReportError.propTypes = {
/**
* View for displaying uncaught error messages.
*/
-const ErrorView = ({ location: { state }, history }) => {
+const ErrorView = () => {
const [tooltipOpen, setTooltipOpen] = useState(false);
-
- const { from: { pathname, search } = {} } = state;
-
+ const location = useLocation();
+ const state = location.state ?? {};
const {
error: {
@@ -54,19 +51,6 @@ const ErrorView = ({ location: { state }, history }) => {
} = {},
} = state;
- if (name === 'AuthenticationError') {
- const savedLocation = {
- pathname,
- search,
- };
- localStorage.setItem('savedLocation', JSON.stringify(savedLocation));
-
- history.push({
- pathname: '/login',
- state: { from: { pathname, search } },
- });
- }
-
const jiraLink = Ticket/Issue;
let errorDetails = `Error Details (Please include in error reports)
@@ -142,9 +126,4 @@ error text: ${message}`;
};
-ErrorView.propTypes = {
- history: PropTypes.object.isRequired,
- location: LocationPropType.isRequired,
-};
-
export default ErrorView;
diff --git a/src/views/LoginView/index.js b/src/views/LoginView/index.js
deleted file mode 100644
index 2d3fa5de2..000000000
--- a/src/views/LoginView/index.js
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * @module /views/LoginView
- */
-import React from 'react';
-
-import { SecurityContext } from '@/components/SecurityContext';
-import { HistoryPropType, LocationPropType } from '@/components/types';
-import api from '@/services/api';
-import {
- getReferrerUri, isAuthenticated, isAuthorized,
- keycloak, login,
-} from '@/services/auth';
-import util from '@/services/util';
-
-/**
- * View to handle user authentication. Redirected to if at any point during use
- * the application receives a 401 error code from the server. Logs in by posting
- * user credentials to the api authentication endpoint, and stores the returned
- * token in browser localstorage.
- */
-class LoginView extends React.Component {
- static contextType = SecurityContext;
-
- static propTypes = {
- history: HistoryPropType.isRequired,
- location: LocationPropType.isRequired,
- };
-
- constructor(props) {
- super(props);
- this.controllers = [];
- }
-
- /**
- * Sends user to log in to keycloak then checks token validity against api.
- * Redirects to /query if successful, displays unauthorized message
- * otherwise.
- */
- async componentDidMount() {
- const {
- setAuthorizationToken, setAuthenticationToken,
- } = this.context;
- const { history, location } = this.props;
- let from;
-
- try {
- from = location.state.from.pathname + location.state.from.search;
- } catch (err) {
- from = getReferrerUri() || '/query';
- }
-
-
- if (!isAuthenticated(this.context)) {
- try {
- await login(from);
- setAuthenticationToken(keycloak.token);
- } catch (err) {
- // redirect to the error page
- history.push('/error', { error: { name: err.name || err, message: err.message || err } });
- return;
- }
- }
-
- if (!isAuthorized(this.context)) {
- const call = api.post('/token', { keyCloakToken: keycloak.token });
- this.controllers.push(call);
-
- try {
- const response = await call.request();
- setAuthorizationToken(response.kbToken);
- } catch (error) {
- // redirect to the error page
- console.error(error);
- util.handleErrorSaveLocation(error, history);
- return;
- }
- }
- const savedLocation = JSON.parse(localStorage.getItem('savedLocation'));
-
- if (savedLocation) {
- const { pathname, search } = savedLocation;
- localStorage.removeItem('savedLocation');
- history.push({
- pathname,
- search,
- });
- } else {
- history.push(from);
- }
- }
-
- componentWillUnmount() {
- this.controllers.forEach(c => c.abort());
- }
-
- render() {
- return null;
- }
-}
-
-export default LoginView;
diff --git a/src/views/MainView/components/MainAppBar.js b/src/views/MainView/components/MainAppBar.js
index 25eb0cde5..dd3c17d5e 100644
--- a/src/views/MainView/components/MainAppBar.js
+++ b/src/views/MainView/components/MainAppBar.js
@@ -16,7 +16,6 @@ import MenuIcon from '@material-ui/icons/Menu';
import PersonIcon from '@material-ui/icons/Person';
import PropTypes from 'prop-types';
import React, {
- useEffect,
useRef,
useState,
} from 'react';
@@ -24,18 +23,15 @@ import {
Link,
} from 'react-router-dom';
-import {
- getUsername, isAdmin, isAuthenticated,
- logout,
-} from '@/services/auth';
+import { useAuth } from '@/components/Auth';
import MenuLink from './MenuLink';
const MainAppBar = ({
- authorizationToken, authenticationToken, onDrawerChange, drawerOpen, onLinkChange,
+ onDrawerChange, drawerOpen, onLinkChange,
}) => {
const [dropdownAnchorEl, setDropdownAnchorEl] = useState(null);
- const [authOk, setAuthOk] = useState(false);
+ const auth = useAuth();
const dropdown = useRef();
@@ -52,10 +48,6 @@ const MainAppBar = ({
onLinkChange({ isOpen: drawerOpen, activeLink: link });
};
- useEffect(() => {
- setAuthOk(isAuthenticated({ authorizationToken, authenticationToken }));
- }, [authorizationToken, authenticationToken]);
-
return (
- {authOk
- ? getUsername({ authenticationToken, authorizationToken })
+ {auth.isAuthenticated
+ ? auth.username
: 'Logged Out'
}
@@ -108,27 +100,29 @@ const MainAppBar = ({
onClick={handleClickLink}
route="/feedback"
/>
- {isAdmin({ authorizationToken }) && (
+ {auth.isAdmin && (
)}
- {authOk && (
+ {auth.isAuthenticated && (
)}
-
+ {auth.isAuthenticated ? (
+
+ ) : (
+
+ )}
@@ -140,14 +134,10 @@ const MainAppBar = ({
MainAppBar.propTypes = {
onDrawerChange: PropTypes.func.isRequired,
onLinkChange: PropTypes.func.isRequired,
- authenticationToken: PropTypes.string,
- authorizationToken: PropTypes.string,
drawerOpen: PropTypes.bool,
};
MainAppBar.defaultProps = {
- authenticationToken: '',
- authorizationToken: '',
drawerOpen: false,
};
diff --git a/src/views/MainView/components/MainNav.js b/src/views/MainView/components/MainNav.js
index acd745bf2..1dc9dea06 100644
--- a/src/views/MainView/components/MainNav.js
+++ b/src/views/MainView/components/MainNav.js
@@ -20,8 +20,7 @@ import PropTypes from 'prop-types';
import React, { useCallback, useContext, useState } from 'react';
import ActiveLinkContext from '@/components/ActiveLinkContext';
-import { SecurityContext } from '@/components/SecurityContext';
-import { hasWriteAccess, isAdmin } from '@/services/auth';
+import { useAuth } from '@/components/Auth';
import logo from '@/static/gsclogo.svg';
import MenuLink from './MenuLink';
@@ -34,7 +33,7 @@ import MenuLink from './MenuLink';
*/
const MainNav = ({ isOpen, onChange }) => {
const [subMenuOpenLink, setSubMenuOpenLink] = useState('/query');
- const context = useContext(SecurityContext);
+ const auth = useAuth();
const { setActiveLink } = useContext(ActiveLinkContext);
/**
@@ -76,15 +75,15 @@ const MainNav = ({ isOpen, onChange }) => {
} label="Quick Search" onClick={handleClickLink} route="/query" />
} label="Advanced Search" onClick={handleClickLink} route="/query-advanced" />
- {hasWriteAccess(context) && (
+ {auth.hasWriteAccess && (
)}
- {hasWriteAccess(context) && (isOpen && subMenuOpenLink === '/new/ontology') && (
+ {auth.hasWriteAccess && (isOpen && subMenuOpenLink === '/new/ontology') && (
<>
- {isAdmin(context) && (
+ {auth.isAdmin && (
)}
@@ -93,13 +92,13 @@ const MainNav = ({ isOpen, onChange }) => {
>
)}
- {hasWriteAccess(context) && (
+ {auth.hasWriteAccess && (
)}
- {hasWriteAccess(context) && (isOpen && subMenuOpenLink === 'import') && (
+ {auth.hasWriteAccess && (isOpen && subMenuOpenLink === 'import') && (
<>
>
diff --git a/src/views/MainView/components/__tests__/MainNav.test.js b/src/views/MainView/components/__tests__/MainNav.test.js
index c16b44cc3..a750ce5a7 100644
--- a/src/views/MainView/components/__tests__/MainNav.test.js
+++ b/src/views/MainView/components/__tests__/MainNav.test.js
@@ -2,7 +2,7 @@ import { mount } from 'enzyme';
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
-import { SecurityContext } from '@/components/SecurityContext';
+import { AuthContext } from '@/components/Auth';
import MainNav from '../MainNav';
@@ -13,9 +13,9 @@ describe('', () => {
test('correctly renders', () => {
wrapper = mount(
-
+
-
+
,
);
expect(wrapper.find(MainNav)).toHaveLength(1);
diff --git a/src/views/MainView/index.js b/src/views/MainView/index.js
index c39050030..1a5d14c72 100644
--- a/src/views/MainView/index.js
+++ b/src/views/MainView/index.js
@@ -6,21 +6,15 @@ import './index.scss';
import {
CircularProgress,
} from '@material-ui/core';
-import fetchIntercept from 'fetch-intercept';
import React, {
lazy,
- Suspense, useEffect, useState,
+ Suspense, useState,
} from 'react';
import { ReactQueryConfigProvider } from 'react-query';
-import {
- Redirect,
- Route,
- Switch,
-} from 'react-router-dom';
+import { Redirect, Route, Switch } from 'react-router-dom';
import ActiveLinkContext from '@/components/ActiveLinkContext';
-import AuthenticatedRoute from '@/components/AuthenticatedRoute';
-import { SecurityContext } from '@/components/SecurityContext';
+import { AuthenticatedRoute } from '@/components/Auth';
import schema from '@/services/schema';
import MainAppBar from './components/MainAppBar';
@@ -35,7 +29,6 @@ const GraphView = lazy(() => import('@/views/GraphView'));
const ErrorView = lazy(() => import('@/views/ErrorView'));
const FeedbackView = lazy(() => import('@/views/FeedbackView'));
const ImportPubmedView = lazy(() => import('@/views/ImportPubmedView'));
-const LoginView = lazy(() => import('@/views/LoginView'));
const NewRecordView = lazy(() => import('@/views/NewRecordView'));
const NewRecordSelectView = lazy(() => import('@/views/NewRecordSelectView'));
const QuickSearch = lazy(() => import('@/views/QuickSearch'));
@@ -52,102 +45,75 @@ const ABSTRACT_CLASSES = Object.values(schema.schema)
* Entry point to application. Handles routing, app theme, and logged in state.
*/
const Main = () => {
- const [authorizationToken, setAuthorizationToken] = useState('');
- const [authenticationToken, setAuthenticationToken] = useState('');
const [drawerOpen, setDrawerOpen] = useState(false);
const [activeLink, setActiveLink] = useState('');
- useEffect(() => {
- const unregister = fetchIntercept.register({
- request: (fetchUrl, fetchConfig) => {
- if (fetchUrl.startsWith(window._env_.API_BASE_URL)) {
- const newConfig = { ...fetchConfig };
-
- if (!newConfig.headers) {
- newConfig.headers = {};
- }
- newConfig.headers.Authorization = authorizationToken;
- return [fetchUrl, newConfig];
- }
- return [fetchUrl, fetchConfig];
- },
- });
- return unregister;
- }, [authorizationToken]);
-
return (
-
-
-
-
- {
- setDrawerOpen(isOpen);
- }}
- />
- {
- setDrawerOpen(isOpen);
- }}
- />
+ },
+ }}
+ >
+
+
+ {
+ setDrawerOpen(isOpen);
+ }}
+ />
+ {
+ setDrawerOpen(isOpen);
+ }}
+ />
-
- )}>
-
-
-
-
-
-
-
-
-
-
-
-
- m.toLowerCase())].join('|')
- })`}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ )}>
+
+
+
+
+
+
+
+
+
+
+
+ m.toLowerCase())].join('|')
+ })`}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/src/views/RecordView/index.js b/src/views/RecordView/index.js
index 0cbee8d0c..5967b0eac 100644
--- a/src/views/RecordView/index.js
+++ b/src/views/RecordView/index.js
@@ -6,19 +6,17 @@ import {
import propTypes from 'prop-types';
import * as qs from 'qs';
import React, {
- useCallback, useContext, useEffect,
- useState,
+ useCallback, useEffect, useState,
} from 'react';
import useDeepCompareEffect from 'use-deep-compare-effect';
+import { useAuth } from '@/components/Auth';
import RecordForm from '@/components/RecordForm';
-import { SecurityContext } from '@/components/SecurityContext';
import StatementForm from '@/components/StatementForm';
import { HistoryPropType } from '@/components/types';
import { cleanLinkedRecords, FORM_VARIANT, navigateToGraph } from '@/components/util';
import VariantForm from '@/components/VariantForm';
import api from '@/services/api';
-import { hasWriteAccess } from '@/services/auth';
import schema from '@/services/schema';
import util from '@/services/util';
@@ -51,7 +49,7 @@ const getModelFromName = (path = '', modelName = '', variant = FORM_VARIANT.VIEW
const RecordView = (props) => {
const { history, match: { path, params: { rid, modelName: modelNameParam, variant } } } = props;
- const context = useContext(SecurityContext);
+ const auth = useAuth();
const [recordContent, setRecordContent] = useState({});
const [modelName, setModelName] = useState(modelNameParam || '');
@@ -167,7 +165,7 @@ const RecordView = (props) => {
onError={handleError}
onSubmit={handleSubmit}
onTopClick={
- hasWriteAccess(context)
+ auth.hasWriteAccess
? onTopClick
: null
}
@@ -185,7 +183,7 @@ const RecordView = (props) => {
onError={handleError}
onSubmit={handleSubmit}
onTopClick={
- hasWriteAccess(context)
+ auth.hasWriteAccess
? onTopClick
: null
}
diff --git a/src/views/UserProfileView/index.js b/src/views/UserProfileView/index.js
index ed9108a40..9eb88096b 100644
--- a/src/views/UserProfileView/index.js
+++ b/src/views/UserProfileView/index.js
@@ -1,18 +1,14 @@
import './index.scss';
import { formatDistanceToNow } from 'date-fns';
-import React, {
- useContext,
-} from 'react';
+import React from 'react';
+import { useAuth } from '@/components/Auth';
import QueryResultsTable from '@/components/QueryResultsTable';
-import { SecurityContext } from '@/components/SecurityContext';
-import { getUser } from '@/services/auth';
const UserProfileView = () => {
- const context = useContext(SecurityContext);
- const user = getUser(context);
+ const { user } = useAuth();
return (