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 && ( )} - logout()}> - { - authOk - ? 'Logout' - : 'Login' - } - + {auth.isAuthenticated ? ( + auth.logout()}> + Logout + + ) : ( + auth.login()}> + Login + + )}
@@ -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 && ( handleOpen('/new/ontology')}> )} - {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 && ( handleOpen('import')}> )} - {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 (