From f0db0104e8a480dba6214e4819c6131cce938be2 Mon Sep 17 00:00:00 2001 From: Kyubinhan Date: Mon, 25 Jun 2018 17:14:20 -0700 Subject: [PATCH] basic authentication --- .eslintrc | 1 + package-lock.json | 171 ++++++++++++++++++++++ package.json | 3 + src/actionCreators/index.js | 29 +++- src/actions/authActions.js | 21 +++ src/actions/index.js | 1 + src/components/App.js | 57 +++++++- src/components/LandingPage.js | 56 ++++++++ src/components/Login.js | 102 +++++++++++++ src/components/Navbar.js | 105 ++++++++++++++ src/components/ReturnPage.js | 41 ++++++ src/components/common/Avatar.js | 31 ++++ src/components/common/index.js | 6 + src/constants/API.js | 14 +- src/constants/actionTypes.js | 5 +- src/constants/reducerTypes.js | 2 +- src/constants/routes.js | 9 +- src/constants/variables.js | 22 +-- src/reducers/authReducer.js | 37 +++++ src/reducers/rootReducer.js | 16 ++- src/styles/Common.scss | 3 +- src/tests/helpers/setupBrowser.js | 13 ++ src/tests/helpers/utils.js | 18 +++ src/tests/intergration/Home.test.js | 121 ++++++++++++++++ src/utils/authentication.js | 216 +++++++++++++--------------- src/utils/index.js | 3 +- src/utils/user.js | 33 +++++ 27 files changed, 991 insertions(+), 145 deletions(-) create mode 100644 src/actions/authActions.js create mode 100644 src/components/LandingPage.js create mode 100644 src/components/Login.js create mode 100644 src/components/Navbar.js create mode 100644 src/components/ReturnPage.js create mode 100644 src/components/common/Avatar.js create mode 100644 src/components/common/index.js create mode 100644 src/reducers/authReducer.js create mode 100644 src/tests/helpers/setupBrowser.js create mode 100644 src/tests/helpers/utils.js create mode 100644 src/tests/intergration/Home.test.js create mode 100644 src/utils/user.js diff --git a/.eslintrc b/.eslintrc index cff55c614..d356a9e07 100644 --- a/.eslintrc +++ b/.eslintrc @@ -31,6 +31,7 @@ "no-shadow": "off", "quote-props": "off", "max-len": "off", + "react/sort-comp": "off", "no-unused-expressions": "warn", "no-useless-concat": "warn", "block-scoped-var": "error", diff --git a/package-lock.json b/package-lock.json index b0f8d02a8..8eaf44453 100644 --- a/package-lock.json +++ b/package-lock.json @@ -124,6 +124,15 @@ "resolved": "https://registry.npmjs.org/adler32cs/-/adler32cs-0.0.1.tgz", "integrity": "sha1-ninNgIdWTXBVIJztEgaljSLneOs=" }, + "agent-base": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.0.tgz", + "integrity": "sha512-c+R/U5X+2zz2+UCrCFv6odQzJdoqI+YecuhnAJLa1zYaMc13zPfwMwZrr91Pd1DYNo/yPRbiM4WVf9whgwFsIg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", @@ -443,6 +452,12 @@ "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=" }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "dev": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -486,6 +501,15 @@ "is-buffer": "^1.1.5" } }, + "axios-mock-adapter": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.15.0.tgz", + "integrity": "sha1-+8BoJdgwLJXDM00hAju6mWJV1F0=", + "dev": true, + "requires": { + "deep-equal": "^1.0.1" + } + }, "axobject-query": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-0.1.0.tgz", @@ -1630,6 +1654,12 @@ "isarray": "^1.0.0" } }, + "buffer-from": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", + "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==", + "dev": true + }, "buffer-indexof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", @@ -3280,6 +3310,15 @@ "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==" }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "^4.0.3" + } + }, "es6-set": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", @@ -3914,6 +3953,32 @@ } } }, + "extract-zip": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", + "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", + "dev": true, + "requires": { + "concat-stream": "1.6.2", + "debug": "2.6.9", + "mkdirp": "0.5.1", + "yauzl": "2.4.1" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + } + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -3980,6 +4045,15 @@ "ua-parser-js": "^0.7.9" } }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -6659,6 +6733,27 @@ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, + "https-proxy-agent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", + "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", + "dev": true, + "requires": { + "agent-base": "^4.1.0", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, "iconv-lite": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", @@ -8299,6 +8394,11 @@ "lodash._objecttypes": "~2.4.1" } }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, "lodash.keys": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", @@ -9815,6 +9915,12 @@ "worker-loader": "^1.1.0" } }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -11187,6 +11293,12 @@ "ipaddr.js": "1.6.0" } }, + "proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=", + "dev": true + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -11251,6 +11363,39 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, + "puppeteer": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.5.0.tgz", + "integrity": "sha512-eELwFtFxL+uhmg4jPZOZXzSrPEYy4CaYQNbcchBbfxY+KjMpnv6XGf/aYWaQG49OTpfi2/DMziXtDM8XuJgoUA==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "extract-zip": "^1.6.6", + "https-proxy-agent": "^2.2.1", + "mime": "^2.0.3", + "progress": "^2.0.0", + "proxy-from-env": "^1.0.0", + "rimraf": "^2.6.1", + "ws": "^5.1.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "mime": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz", + "integrity": "sha512-OEUllcVoydBHGN1z84yfQDimn58pZNNNXgZlHXSboxMlFvgI6MXSWpWKpFRra7H1HxpVhHTkrghfRW49k6yjeg==", + "dev": true + } + } + }, "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -11967,6 +12112,14 @@ "deep-diff": "^0.3.5" } }, + "redux-mock-store": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.3.tgz", + "integrity": "sha512-ryhkkb/4D4CUGpAV2ln1GOY/uh51aczjcRz9k2L2bPx/Xja3c5pSGJJPyR25GNVRXtKIExScdAgFdiXp68GmJA==", + "requires": { + "lodash.isplainobject": "^4.0.6" + } + }, "redux-thunk": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.2.0.tgz", @@ -15505,6 +15658,15 @@ "signal-exit": "^3.0.2" } }, + "ws": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.1.tgz", + "integrity": "sha512-2NkHdPKjDBj3CHdnAGNpmlliryKqF+n9MYXX7/wsVC4yqYocKreKNjydPDvT3wShAZnndlM0RytEfTALCDvz7A==", + "dev": true, + "requires": { + "async-limiter": "~1.0.0" + } + }, "xdg-basedir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", @@ -15594,6 +15756,15 @@ "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" } } + }, + "yauzl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", + "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", + "dev": true, + "requires": { + "fd-slicer": "~1.0.1" + } } } } diff --git a/package.json b/package.json index 38c63b3bd..024512c3c 100644 --- a/package.json +++ b/package.json @@ -27,16 +27,19 @@ "redux": "^3.7.2", "redux-logger": "^3.0.6", "redux-thunk": "^2.2.0", + "redux-mock-store": "^1.5.3", "semantic-ui-react": "^0.78.2" }, "devDependencies": { "@types/jest": "^22.2.2", + "axios-mock-adapter": "^1.15.0", "eslint": "^4.19.1", "eslint-config-airbnb": "^16.1.0", "eslint-plugin-import": "^2.10.0", "eslint-plugin-jsx-a11y": "^6.0.3", "eslint-plugin-react": "^7.7.0", "gulp": "^3.9.1", + "puppeteer": "^1.5.0", "semantic-ui": "^2.3.1" }, "scripts": { diff --git a/src/actionCreators/index.js b/src/actionCreators/index.js index 9f883c40b..401e50130 100644 --- a/src/actionCreators/index.js +++ b/src/actionCreators/index.js @@ -1,10 +1,35 @@ // import axios from 'axios'; import { normalize } from 'normalizr'; +import { axios, saveUserProfileInLocal } from '../utils'; import * as schema from './schema'; import * as api from '../api'; -import { request, success, successPagenated, error, storeAgreement, storePlan } from '../actions'; -import { getAgreementsIsFetching } from '../reducers/rootReducer'; +import { + request, success, successPagenated, error, + storeAgreement, storePlan, storeUser, +} from '../actions'; +import { getAgreementsIsFetching, getToken } from '../reducers/rootReducer'; import * as reducerTypes from '../constants/reducerTypes'; +import * as API from '../constants/API'; + +const createRequestHeader = state => ({ + headers: { + 'Authorization': `Bearer ${getToken(state)}`, + 'content-type': 'application/json', + }, +}); + +export const getUserProfile = () => (dispatch, getState) => { + return axios.get(API.GET_USER_PROFILE_ENDPOINT, createRequestHeader(getState())).then( + (response) => { + const user = response.data; + dispatch(storeUser(user)); + saveUserProfileInLocal(user); + }, + (err) => { + throw err; + }, + ); +}; export const searchAgreements = filter => (dispatch, getState) => { if (getAgreementsIsFetching(getState(), filter)) { diff --git a/src/actions/authActions.js b/src/actions/authActions.js new file mode 100644 index 000000000..2ee9a900c --- /dev/null +++ b/src/actions/authActions.js @@ -0,0 +1,21 @@ +import * as actionTypes from '../constants/actionTypes'; + +export const storeAuthData = data => ( + { + type: actionTypes.STORE_SSO_AUTH_DATA, + data, + } +); + +export const storeUser = user => ( + { + type: actionTypes.STORE_USER, + user, + } +); + +export const signOut = () => ( + { + type: actionTypes.SIGN_OUT, + } +); diff --git a/src/actions/index.js b/src/actions/index.js index d650aa84e..be1023c4f 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -1,2 +1,3 @@ export * from './storeActions'; export * from './networkActions'; +export * from './authActions'; diff --git a/src/components/App.js b/src/components/App.js index ba5754b59..c2098ded0 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -1,17 +1,68 @@ import React, { Component } from 'react'; import { Provider } from 'react-redux'; -import { BrowserRouter, Route } from 'react-router-dom'; +import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom'; import configureStore from '../configureStore'; -import Home from '../components/Home'; +import * as Routes from '../constants/routes'; +import Home from './Home'; +import Login from './Login'; +import ReturnPage from './ReturnPage'; +import LandingPage from './LandingPage'; +import { getAuthData } from '../reducers/rootReducer'; export const store = configureStore(); +// console.log(store.getState()); + +/* eslint-disable react/prop-types */ /* eslint-disable react/prefer-stateless-function */ +// const AdminRoute = ({ component: Component, ...rest }) => ( +// { +// if (user && user.isAdmin) { +// return ; +// } +// return ; +// } +// } +// /> +// ); + +const PrivateRoute = ({ component: Component, ...rest }) => ( + { // props = { match:{...}, history:{...}, location:{...} } + if (getAuthData(store.getState())) { + return ; + } + return ; + } + } + /> +); + +const PublicRoute = ({ component: Component, ...rest }) => ( + { + if (getAuthData(store.getState())) { + return ; + } + return ; + }} + /> +); + class App extends Component { render() { return ( - + + + + + ()} /> + ); diff --git a/src/components/LandingPage.js b/src/components/LandingPage.js new file mode 100644 index 000000000..c99db145f --- /dev/null +++ b/src/components/LandingPage.js @@ -0,0 +1,56 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import Navbar from './Navbar'; +import { userHaveRole, isUserActive } from '../utils'; +import { getUserProfile } from '../actionCreators'; +import { getUser } from '../reducers/rootReducer'; +// import { getReferences, getZones } from '../actions/commonActions'; + +const propTypes = { + component: PropTypes.func.isRequired, + user: PropTypes.shape({}), + getUserProfile: PropTypes.func.isRequired, + // getZones: PropTypes.func.isRequired, + // getReferences: PropTypes.func.isRequired, +}; +const defaultProps = { + user: undefined, +}; + +export class LandingPage extends Component { + componentDidMount() { + // const { getReferences, getZones } = this.props; + // getReferences(); + // getZones(); + this.props.getUserProfile(); + } + + render() { + const { component: Component, user, ...rest } = this.props; + + return ( +
+ + { !isUserActive(user) && +
This account is not active in the server.
+ } + { userHaveRole(user) && + + } +
+
+ ); + } +} + +const mapStateToProps = state => ( + { + user: getUser(state), + } +); + +LandingPage.propTypes = propTypes; +LandingPage.defaultProps = defaultProps; +// export default connect(mapStateToProps, { getReferences, getZones })(LandingPage); +export default connect(mapStateToProps, { getUserProfile })(LandingPage); diff --git a/src/components/Login.js b/src/components/Login.js new file mode 100644 index 000000000..bd994f761 --- /dev/null +++ b/src/components/Login.js @@ -0,0 +1,102 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { Button } from 'semantic-ui-react'; + +import { SSO_LOGIN_ENDPOINT, SSO_IDIR_LOGIN_ENDPOINT, SSO_BCEID_LOGIN_ENDPOINT } from '../constants/API'; +import { ELEMENT_ID, IMAGE_SRC } from '../constants/variables'; +import { HOME } from '../constants/routes'; +import { storeAuthData } from '../actions'; + +const propTypes = { + storeAuthData: PropTypes.func.isRequired, + history: PropTypes.shape({ push: PropTypes.func }).isRequired, +}; + +export class Login extends Component { + // Sets up localstorage listener for cross-tab communication since spotify authentication requires the user to be redirected + // to another page and then redirected back to a return URL with the token. + componentDidMount() { + window.addEventListener('storage', this.storageEventListener); + } + + componentWillUnmount() { + window.removeEventListener('storage', this.storageEventListener); + } + + storageEventListener = (event) => { + const authData = JSON.parse(localStorage.getItem(event.key)); + // store the auth data in Redux + this.props.storeAuthData(authData); + // redirect to /home + this.props.history.push(HOME); + } + + openNewTab = link => window.open(link, '_black') + onLoginBtnClick = () => this.openNewTab(SSO_LOGIN_ENDPOINT) + onIdirLoginBtnClick = () => this.openNewTab(SSO_IDIR_LOGIN_ENDPOINT) + onBceidLoginBtnClick = () => this.openNewTab(SSO_BCEID_LOGIN_ENDPOINT) + + render() { + return ( +
+ login-img + +
+ My Range Application +
+ +
+ + + + + +
+ + Is your password expired? + +
+ ); + } +} + +const mapStateToProps = state => ( + { + } +); + +Login.propTypes = propTypes; +export default connect(mapStateToProps, { storeAuthData })(Login); diff --git a/src/components/Navbar.js b/src/components/Navbar.js new file mode 100644 index 000000000..a26f4ea3c --- /dev/null +++ b/src/components/Navbar.js @@ -0,0 +1,105 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { NavLink, Link } from 'react-router-dom'; +import { Avatar } from './common'; +import * as Routes from '../constants/routes'; +import { IMAGE_SRC, ELEMENT_ID } from '../constants/variables'; +import { SITEMINDER_LOGOUT_ENDPOINT } from '../constants/API'; +import { getUser } from '../reducers/rootReducer'; +import { isUserAdmin, isUserActive } from '../utils'; +import { signOut } from '../actions'; + +const propTypes = { + user: PropTypes.shape({}), + history: PropTypes.shape({ push: PropTypes.func }).isRequired, + signOut: PropTypes.func.isRequired, +}; +const defaultProps = { + user: undefined, +}; + +/* eslint-disable jsx-a11y/anchor-is-valid */ +export class Navbar extends Component { + onLogoutBtnClick = () => { + // clear the local storage in the browser + localStorage.clear(); + // remove the auth and user data in Redux + this.props.signOut(); + // redirect to the login page + this.props.history.push('/login'); + + // open a new tab for signing out from SiteMinder which is Gov's auth platform + // once it returns back, it will sign out from SSO which will happen in ReturnPage.js + window.open(SITEMINDER_LOGOUT_ENDPOINT, '_blank'); + } + + render() { + const { user } = this.props; + + return ( + + ); + } +} + +const mapStateToProps = state => ( + { + user: getUser(state), + } +); +Navbar.propTypes = propTypes; +Navbar.defaultProps = defaultProps; +export default connect(mapStateToProps, { signOut })(Navbar); diff --git a/src/components/ReturnPage.js b/src/components/ReturnPage.js new file mode 100644 index 000000000..d0b893d19 --- /dev/null +++ b/src/components/ReturnPage.js @@ -0,0 +1,41 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { parseQuery, getTokenFromSSO, saveAuthDataInLocal } from '../utils'; +import { SSO_LOGOUT_ENDPOINT } from '../constants/API'; + +const propTypes = { + location: PropTypes.shape({}), +}; +const defaultProps = { + location: {}, +}; + +class ReturnPage extends Component { + componentDidMount() { + const { location } = this.props; + // grab the code from the redirect url + const { type, code } = parseQuery(location.search); + if (type === 'login' && code) { + const tokenReceived = (response) => { + saveAuthDataInLocal(response); + window.close(); + }; + getTokenFromSSO(code).then(tokenReceived); + } else if (type === 'smlogout') { + // just returned from SiteMinder, sign out from SSO this time + window.open(SSO_LOGOUT_ENDPOINT, '_self'); + } else if (type === 'logout') { + // done signing out close this tab + window.close(); + } + } + + render() { + return ( +
Return Page
+ ); + } +} +ReturnPage.propTypes = propTypes; +ReturnPage.defaultProps = defaultProps; +export default ReturnPage; diff --git a/src/components/common/Avatar.js b/src/components/common/Avatar.js new file mode 100644 index 000000000..422622b04 --- /dev/null +++ b/src/components/common/Avatar.js @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { getUserInitial } from '../../utils'; + +const propTypes = { + user: PropTypes.shape({}), + className: PropTypes.string, +}; + +const defaultProps = { + user: { + familyName: 'N', + givenName: 'P', + }, + className: '', +}; + +const Avatar = ({ className, user }) => ( + ( +
+
+ {getUserInitial(user)} +
+
+ ) +); + +Avatar.propTypes = propTypes; +Avatar.defaultProps = defaultProps; +export default Avatar; diff --git a/src/components/common/index.js b/src/components/common/index.js new file mode 100644 index 000000000..32af573f8 --- /dev/null +++ b/src/components/common/index.js @@ -0,0 +1,6 @@ +export { default as Avatar } from './Avatar'; +// export { default as TextField } from './TextField'; +// export { default as Status } from './Status'; +// export { default as Banner } from './Banner'; +// export { default as ConfirmationModal } from './ConfirmationModal'; +// export { default as Loading } from './Loading'; diff --git a/src/constants/API.js b/src/constants/API.js index 676da27a6..7dd2a6bb4 100644 --- a/src/constants/API.js +++ b/src/constants/API.js @@ -2,20 +2,22 @@ export const SSO_BASE_URL = 'https://dev-sso.pathfinder.gov.bc.ca'; export const SSO_REALM_NAME = 'mobile'; export const SSO_BASE_AUTH_ENDPOINT = `${SSO_BASE_URL}/auth/realms/mobile/protocol/openid-connect`; export const SSO_CLIENT_ID = 'range-test'; -export const SSO_LOGIN_REDIRECT_URI = `${window.location.origin}/login`; +export const SSO_LOGIN_REDIRECT_URI = `${window.location.origin}/return-page?type=login`; export const SSO_LOGIN_ENDPOINT = `${SSO_BASE_AUTH_ENDPOINT}/auth?response_type=code&client_id=${SSO_CLIENT_ID}&redirect_uri=${SSO_LOGIN_REDIRECT_URI}`; +export const SSO_IDIR_LOGIN_ENDPOINT = `${SSO_LOGIN_ENDPOINT}&kc_idp_hint=idir`; +export const SSO_BCEID_LOGIN_ENDPOINT = `${SSO_LOGIN_ENDPOINT}&kc_idp_hint=bceid`; -export const SSO_LOGOUT_REDIRECT_URI = `${window.location.origin}/logout`; +export const SSO_LOGOUT_REDIRECT_URI = `${window.location.origin}/return-page?type=logout`; export const SSO_LOGOUT_ENDPOINT = `${SSO_BASE_AUTH_ENDPOINT}/logout?redirect_uri=${SSO_LOGOUT_REDIRECT_URI}`; -export const SITEMINDER_LOGOUT_REDIRECT_URI = `${window.location.origin}/logout?smret=1`; +export const SITEMINDER_LOGOUT_REDIRECT_URI = `${window.location.origin}/return-page?type=smlogout`; export const SITEMINDER_LOGOUT_ENDPOINT = `https://logontest.gov.bc.ca/clp-cgi/logoff.cgi?returl=${SITEMINDER_LOGOUT_REDIRECT_URI}&retnow=1`; export const GET_TOKEN_FROM_SSO = `/auth/realms/${SSO_REALM_NAME}/protocol/openid-connect/token`; export const REFRESH_TOKEN_FROM_SSO = `/auth/realms/${SSO_REALM_NAME}/protocol/openid-connect/token`; -const DEV_API_BASE_URL = 'http://web-range-myra-dev.pathfinder.gov.bc.ca/api/v1'; +// const DEV_API_BASE_URL = 'http://web-range-myra-dev.pathfinder.gov.bc.ca/api/v1'; // const DEV_API_BASE_URL = 'https://web-range-myra-test.pathfinder.gov.bc.ca/api/v1'; -// const DEV_API_BASE_URL = 'http://localhost:8000/api/v1'; +const DEV_API_BASE_URL = 'http://localhost:8000/api/v1'; // const DEV_API_BASE_URL = 'http://10.10.10.191:8000/api/v1'; export const API_BASE_URL = (process.env.NODE_ENV === 'production') @@ -38,4 +40,4 @@ export const CREATE_RUP_SCHEDULE_ENDPOINT = planId => `/plan/${planId}/schedule` export const UPDATE_RUP_SCHEDULE_ENDPOINT = (planId, scheduleId) => `/plan/${planId}/schedule/${scheduleId}`; export const DELETE_RUP_SCHEDULE_ENDPOINT = (planId, scheduleId) => `/plan/${planId}/schedule/${scheduleId}`; export const DELETE_RUP_SCHEDULE_ENTRY_ENDPOINT = (planId, scheduleId, entryId) => `/plan/${planId}/schedule/${scheduleId}/entry/${entryId}`; -export const UPDATE_AH_CLIENT_ID_ENDPOINT = (userId, clientId) => `/user/${userId}/client/${clientId}`; +export const UPDATE_AH_CLIENT_ID_ENDPOINT = (userId, clientId) => `/user/${userId}/client/${clientId}`; \ No newline at end of file diff --git a/src/constants/actionTypes.js b/src/constants/actionTypes.js index c4c64f1ad..007175b2d 100644 --- a/src/constants/actionTypes.js +++ b/src/constants/actionTypes.js @@ -1,5 +1,6 @@ -// export const AUTHENTICATE_USER = 'AUTHENTICATE_USER'; -// export const REVOKE_AUTHENTICATION = 'REVOKE_AUTHENTICATION'; +export const STORE_SSO_AUTH_DATA = 'STORE_SSO_AUTH_DATA'; +export const STORE_USER = 'STORE_USER'; +export const SIGN_OUT = 'SIGN_OUT'; export const REQUEST = 'REQUEST'; export const SUCCESS = 'SUCCESS'; diff --git a/src/constants/reducerTypes.js b/src/constants/reducerTypes.js index 5d5c77d4c..531d31a4b 100644 --- a/src/constants/reducerTypes.js +++ b/src/constants/reducerTypes.js @@ -1,4 +1,4 @@ -export const AUTHENTICATION = 'AUTHENTICATION'; +export const AUTH = 'AUTH'; export const AGREEMENTS = 'AGREEMENTS'; export const PLAN = 'PLAN'; diff --git a/src/constants/routes.js b/src/constants/routes.js index d980e906c..48ae8d658 100644 --- a/src/constants/routes.js +++ b/src/constants/routes.js @@ -1 +1,8 @@ -export const HOME = '/'; +export const HOME = '/home'; +export const LOGIN = '/login'; +export const LOGOUT = '/logout'; + +export const RANGE_USE_PLAN = '/range-use-plan'; +export const MANAGE_ZONE = '/manage-zone'; +export const EXPORT_PDF = '/export-pdf'; +export const MANAGE_CLIENT = '/manage-client'; diff --git a/src/constants/variables.js b/src/constants/variables.js index 835337769..30caa48e3 100644 --- a/src/constants/variables.js +++ b/src/constants/variables.js @@ -39,21 +39,23 @@ export const DATE_FORMAT = { CLIENT_SIDE: 'MMMM D, YYYY', }; -export const ROLE = { +export const USER_ROLE = { ADMINISTRATOR: 'myra_admin', RANGE_OFFICER: 'myra_range_officer', AGREEMENT_HOLDER: 'myra_client', }; export const ELEMENT_ID = { - GRAZING_SCHEDULE_ELEMENT_ID: 'rup__grazing-schedule', - RUP_ZONE_DROPDOWN_ELEMENT_ID: 'rup__zone-dropdown', - RUP_STICKY_HEADER_ELEMENT_ID: 'rup-sticky-header', - SIGN_OUT_ELEMENT_ID: 'sign-out', - MANAGE_ZONE_ZONES_DROPDOWN_ELEMENT_ID: 'manage-zone__zone-dropdown', - MANAGE_ZONE_CONTACTS_DROPDOWN_ELEMENT_ID: 'manage-zone__contact-dropdown', - LOGIN_BUTTON_ELEMENT_ID: 'login-button', + GRAZING_SCHEDULE: 'rup__grazing-schedule', + RUP_ZONE_DROPDOWN: 'rup__zone-dropdown', + RUP_STICKY_HEADER: 'rup-sticky-header', + SIGN_OUT: 'sign-out', + MANAGE_ZONE_ZONES_DROPDOWN: 'manage-zone__zone-dropdown', + MANAGE_ZONE_CONTACTS_DROPDOWN: 'manage-zone__contact-dropdown', + LOGIN_BUTTON: 'login-button', + LOGIN_IDIR_BUTTON: 'login-idir-button', + LOGIN_BCEID_BUTTON: 'login-bceid-button', SEARCH_TERM: 'searchTerm', - MANAGE_CLIENT_USERS_DROPDOWN_ELEMENT_ID: 'manage-client__users-dropdown', - MANAGE_CLIENT_CLIENTS_DROPDOWN_ELEMENT_ID: 'manage-client__clients-dropdown', + MANAGE_CLIENT_USERS_DROPDOWN: 'manage-client__users-dropdown', + MANAGE_CLIENT_CLIENTS_DROPDOWN: 'manage-client__clients-dropdown', }; diff --git a/src/reducers/authReducer.js b/src/reducers/authReducer.js new file mode 100644 index 000000000..c1c6f1d22 --- /dev/null +++ b/src/reducers/authReducer.js @@ -0,0 +1,37 @@ +import { STORE_SSO_AUTH_DATA, STORE_USER, SIGN_OUT } from '../constants/actionTypes'; +import { getAuthAndUserFromLocal } from '../utils'; + +const { user, authData } = getAuthAndUserFromLocal(); +const initialState = { + authData, + user, +}; + +const authReducer = (state = initialState, action) => { + switch (action.type) { + case STORE_SSO_AUTH_DATA: + return { + ...state, + authData: action.data, + }; + case STORE_USER: + return { + ...state, + user: action.user, + }; + case SIGN_OUT: + return { + ...state, + authData: undefined, + user: undefined, + }; + default: + return state; + } +}; + +// Private selectors being name exported +export const getAuthData = state => state.authData; +export const getToken = state => state.authData && state.authData.access_token; +export const getUser = state => state.user; +export default authReducer; diff --git a/src/reducers/rootReducer.js b/src/reducers/rootReducer.js index 31974e103..01133aec0 100644 --- a/src/reducers/rootReducer.js +++ b/src/reducers/rootReducer.js @@ -20,8 +20,10 @@ import { combineReducers } from 'redux'; import * as reducerTypes from '../constants/reducerTypes'; +import { SIGN_OUT } from '../constants/actionTypes'; import agreementReducer, * as fromAgreement from './agreementReducer'; import networkReducer, * as fromNetwork from './networkReducer'; +import authReducer, * as fromAuth from './authReducer'; import planReducer from './planReducer'; // const createReduce @@ -33,11 +35,12 @@ const createReducer = (reducer, name) => (state, action) => { return reducer(state, action); }; -const rootReducer = combineReducers({ +const appReducer = combineReducers({ [reducerTypes.AGREEMENTS]: agreementReducer, [reducerTypes.SEARCH_AGREEMENTS]: createReducer(networkReducer, reducerTypes.SEARCH_AGREEMENTS), [reducerTypes.GET_PLAN]: createReducer(networkReducer, reducerTypes.GET_PLAN), [reducerTypes.PLAN]: planReducer, + [reducerTypes.AUTH]: authReducer, }); // public selectors @@ -45,5 +48,16 @@ export const getAgreements = state => fromAgreement.getAgreements(state[reducerT export const getAgreementIds = state => fromAgreement.getAgreementIds(state[reducerTypes.AGREEMENTS]); export const getAgreementsPagination = state => fromNetwork.getPagination(state[reducerTypes.SEARCH_AGREEMENTS]); export const getAgreementsIsFetching = state => fromNetwork.getIsFetching(state[reducerTypes.SEARCH_AGREEMENTS]); +export const getAuthData = state => fromAuth.getAuthData(state[reducerTypes.AUTH]); +export const getUser = state => fromAuth.getUser(state[reducerTypes.AUTH]); +export const getToken = state => fromAuth.getToken(state[reducerTypes.AUTH]); +const rootReducer = (state, action) => { + // reset the state of a Redux store when users sign out + if (action.type === SIGN_OUT) { + return appReducer(undefined, action); + } + + return appReducer(state, action); +}; export default rootReducer; diff --git a/src/styles/Common.scss b/src/styles/Common.scss index fa30488ce..d752023a5 100644 --- a/src/styles/Common.scss +++ b/src/styles/Common.scss @@ -63,7 +63,8 @@ } .avatar { - width: 40px; + min-width: 40px; + max-width: 40px; height: 40px; border-radius: 50%; background: $primary-light-color; diff --git a/src/tests/helpers/setupBrowser.js b/src/tests/helpers/setupBrowser.js new file mode 100644 index 000000000..27c9881db --- /dev/null +++ b/src/tests/helpers/setupBrowser.js @@ -0,0 +1,13 @@ +import puppeteer from 'puppeteer'; + +const setupBrowser = async () => { + const browser = await puppeteer.launch({ + headlesss: process.env.CI, + devtools: !process.env.CI, + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + const page = await browser.newPage(); + return { browser, page }; +} + +export default setupBrowser; diff --git a/src/tests/helpers/utils.js b/src/tests/helpers/utils.js new file mode 100644 index 000000000..fba3737c8 --- /dev/null +++ b/src/tests/helpers/utils.js @@ -0,0 +1,18 @@ +import { applyMiddleware, createStore } from 'redux'; +import { configureStore } from 'redux-mock-store'; +import rootReducer from '../../reducers/rootReducer'; + +export const flushAllPromises = () => new Promise(resolve => setTimeout(resolve, 0)); + +export const configureMockStore = (middlewares = []) => { + const store = createStore( + rootReducer, + applyMiddleware(...middlewares), + ); + + return store; +}; + +export const configureReduxMockStore = (middlewares = []) => ( + configureStore(...middlewares) +); diff --git a/src/tests/intergration/Home.test.js b/src/tests/intergration/Home.test.js new file mode 100644 index 000000000..3e4ab3021 --- /dev/null +++ b/src/tests/intergration/Home.test.js @@ -0,0 +1,121 @@ +// import React from 'react'; +// import axios from 'axios'; +// import MockAdapter from 'axios-mock-adapter'; +// import { Provider } from 'react-redux'; +// import { mount } from 'enzyme'; +// import thunk from 'redux-thunk'; + +// import Home from '../../components/home/Home'; +// import { authenticateUser } from '../../actions/authenticateActions'; +// import { configureMockStore, flushAllPromises } from '../helpers/utils'; +// import { mockRequestHeader, mockFetchSavedAlbumsResponse, mockArtistResponse } from './mockData'; +// import * as API from '../../constants/api'; + +// let store; +// const mockAxios = new MockAdapter(axios); +// const mockToken = "mockToken"; + +// beforeEach(() => { +// store = configureMockStore([thunk]); +// store.dispatch(authenticateUser('mockToken')); +// mockAxios.reset(); +// }); + +// describe('Integration testing', () => { +// it('Component initializes properly', async () => { +// mockAxios.onGet(API.GET_LIBRARY_LINK(), mockRequestHeader).reply(200, mockFetchSavedAlbumsResponse); +// const wrapper = mount(); +// await flushAllPromises(); +// // Forces a re-render +// wrapper.update(); + +// expect(store.getState().LIBRARY.albumIds).toHaveLength(3); +// }); +// describe('Browse functionalities', () => { +// const query = "test"; +// const filterType = "album"; +// const mockSearchAlbumResponse = { +// albums: { +// items: mockFetchSavedAlbumsResponse.items.map(item => item.album), +// }, +// }; +// let wrapper; + +// beforeEach(() => { +// mockAxios +// .onGet(API.GET_LIBRARY_LINK(), mockRequestHeader).reply(200, mockFetchSavedAlbumsResponse) +// .onGet(API.SEARCH_LINK(query, filterType), mockRequestHeader).reply(200, mockSearchAlbumResponse); + +// wrapper = mount( +// +// +// +// ); +// wrapper.find('.form-control').simulate('change', {target: { value: 'test' }}); +// wrapper.find('button').simulate('submit'); +// }); + +// it('Submits query and searchs for albums', async () => { +// await flushAllPromises(); +// wrapper.update(); + +// expect(wrapper.find('.card')).toHaveLength(3); +// expect(wrapper.find('.artist-filter button')).toHaveLength(3); + +// expect(wrapper.find('.artist-filter button').at(0).text()).toEqual('All'); +// expect(wrapper.find('.artist-filter button').at(1).text()).toEqual('A.A.L'); +// expect(wrapper.find('.artist-filter button').at(2).text()).toEqual('Alvvays'); + +// expect(wrapper.find('.card button').at(0).text()).toEqual("Remove"); +// expect(wrapper.find('.card button').at(1).text()).toEqual("Remove"); +// expect(wrapper.find('.card button').at(2).text()).toEqual("Remove"); +// }); + +// it('Favoriting/Unfavoriting an album adds/removes the album from the library', async () => { +// const albumId = "1uzfGk9vxMXfaZ2avqwxod"; +// mockAxios +// .onPut(API.ADD_LIBRARY_ALBUM_LINK(), [albumId]).reply(200, { success: true }) +// .onAny(API.DELETE_LIBRARY_ALBUM_LINK(albumId), mockRequestHeader).reply(200, { success: true }); + +// await flushAllPromises(); +// wrapper.update(); +// //Should have the same 3 albums in library and search results +// expect(store.getState().LIBRARY.albumIds).toHaveLength(3); + +// //Unfavorite the first album +// wrapper.find('.card button').at(0).simulate('click'); + +// await flushAllPromises(); +// wrapper.update(); +// //Now there should be only 2 albums in library +// expect(store.getState().LIBRARY.albumIds).toHaveLength(2); +// expect(store.getState().LIBRARY.albumIds.indexOf("1uzfGk9vxMXfaZ2avqwxod")).toEqual(-1); + +// //Favorite the same album again +// wrapper.find('.card button').at(0).simulate('click'); + +// await flushAllPromises(); +// wrapper.update(); + +// //Those albums should reappear in the library again +// expect(store.getState().LIBRARY.albumIds).toHaveLength(3); +// expect(store.getState().LIBRARY.albumIds.indexOf("1uzfGk9vxMXfaZ2avqwxod")).toBeGreaterThan(0); +// }); + +// it('Click an artist filter will filter the search results', async () => { +// const artistId = "329iU5aUf9pGiYFbjE9xqQ"; +// mockAxios.onGet(API.GET_ARTIST_LINK(artistId), mockRequestHeader).reply(200, mockArtistResponse); + +// await flushAllPromises(); +// wrapper.update(); +// expect(wrapper.find('.card')).toHaveLength(3); + +// wrapper.find('.artist-filter button').filterWhere(node => node.text() === 'A.A.L').simulate('click'); + +// await flushAllPromises(); +// wrapper.update(); +// expect(wrapper.find('.card')).toHaveLength(1); +// expect(wrapper.find('.artist-info')).toHaveLength(1); +// }); +// }); +// }); diff --git a/src/utils/authentication.js b/src/utils/authentication.js index 9716922d9..2d8ac3a16 100644 --- a/src/utils/authentication.js +++ b/src/utils/authentication.js @@ -25,51 +25,37 @@ import { SSO_LOGIN_REDIRECT_URI, SSO_CLIENT_ID, GET_TOKEN_FROM_SSO, - REFRESH_TOKEN_FROM_SSO, - GET_USER_PROFILE_ENDPOINT, + // REFRESH_TOKEN_FROM_SSO, + // GET_USER_PROFILE_ENDPOINT, } from '../constants/API'; import { saveDataInLocal, getDataFromLocal } from './localStorage'; import { stringifyQuery } from './queryString'; import { LOCAL_STORAGE_KEY } from '../constants/variables'; -const getRefreshTokenFromLocal = () => { - const data = getDataFromLocal(LOCAL_STORAGE_KEY.AUTH); - return data && data.refresh_token; +/** + * this method is called immediately at the very beginning in the auth reducer + * to initialize 'user' object in App.jsx. It checks whether + * the user signs in previously by looking at localStorage and + * it returns either null or an object retrieved from localStorage + */ +export const getAuthAndUserFromLocal = () => { + const user = getDataFromLocal(LOCAL_STORAGE_KEY.USER); + const authData = getDataFromLocal(LOCAL_STORAGE_KEY.AUTH); + return { authData, user }; }; -const getJWTDataFromLocal = () => { - const data = getDataFromLocal(LOCAL_STORAGE_KEY.AUTH); - return data && data.jwtData; -}; +export const saveAuthDataInLocal = (response) => { + const data = { ...response.data }; + data.jwtData = jwtDecode(data.access_token); -const isTokenExpired = () => { - const jstData = getJWTDataFromLocal(); - if (jstData) { - return (new Date() / 1000) > jstData.exp; - } - return false; + saveDataInLocal(LOCAL_STORAGE_KEY.AUTH, data); }; -const refreshAccessToken = (refreshToken, isRetry) => { - const data = { - refresh_token: refreshToken, - grant_type: 'refresh_token', - redirect_uri: SSO_LOGIN_REDIRECT_URI, - client_id: SSO_CLIENT_ID, - }; - - // make an application/x-www-form-urlencoded request with axios - // pass isRetry in config so that it only tries to refresh once. - return axios({ - method: 'post', - baseURL: SSO_BASE_URL, - url: REFRESH_TOKEN_FROM_SSO, - data: stringifyQuery(data), - isRetry, - }); +export const saveUserProfileInLocal = (newUser) => { + saveDataInLocal(LOCAL_STORAGE_KEY.USER, newUser); }; -export const getTokenFromRemote = (code) => { +export const getTokenFromSSO = (code) => { const data = { code, grant_type: 'authorization_code', @@ -85,30 +71,47 @@ export const getTokenFromRemote = (code) => { }); }; -const setAxiosAuthHeader = (data) => { - const tokenType = data && data.token_type; - const accessToken = data && data.access_token; - axios.defaults.headers.common.Authorization = tokenType && accessToken && `${tokenType} ${accessToken}`; -}; +// const getRefreshTokenFromLocal = () => { +// const data = getDataFromLocal(LOCAL_STORAGE_KEY.AUTH); +// return data && data.refresh_token; +// }; -/** - * this method is called immediately at the very beginning in the auth reducer - * to initialize 'user' object in App.jsx. It checks whether - * the user signs in previously by looking at localStorage and - * it returns either null or an object retrieved from localStorage - */ -// export const initializeUser = () => { -// let user = null; +// const getJWTDataFromLocal = () => { +// const data = getDataFromLocal(LOCAL_STORAGE_KEY.AUTH); +// return data && data.jwtData; +// }; -// const userData = getDataFromLocal(LOCAL_STORAGE_KEY.USER); -// if (userData) { -// user = new User(userData); +// const isTokenExpired = () => { +// const jstData = getJWTDataFromLocal(); +// if (jstData) { +// return (new Date() / 1000) > jstData.exp; // } -// const authData = getDataFromLocal(LOCAL_STORAGE_KEY.AUTH); -// if (authData) { -// setAxiosAuthHeader(authData); -// } -// return user; +// return false; +// }; + +// const refreshAccessToken = (refreshToken, isRetry) => { +// const data = { +// refresh_token: refreshToken, +// grant_type: 'refresh_token', +// redirect_uri: SSO_LOGIN_REDIRECT_URI, +// client_id: SSO_CLIENT_ID, +// }; + +// // make an application/x-www-form-urlencoded request with axios +// // pass isRetry in config so that it only tries to refresh once. +// return axios({ +// method: 'post', +// baseURL: SSO_BASE_URL, +// url: REFRESH_TOKEN_FROM_SSO, +// data: stringifyQuery(data), +// isRetry, +// }); +// }; + +// const setAxiosAuthHeader = (data) => { +// const tokenType = data && data.token_type; +// const accessToken = data && data.access_token; +// axios.defaults.headers.common.Authorization = tokenType && accessToken && `${tokenType} ${accessToken}`; // }; /** @@ -117,44 +120,23 @@ const setAxiosAuthHeader = (data) => { * set auth header in Axios and store auth data in localstorage * after succesfully authenticate */ -export const onAuthenticated = (response) => { - if (response && response.data) { - const { data } = response; - data.jwtData = jwtDecode(data.access_token); - - saveDataInLocal(LOCAL_STORAGE_KEY.AUTH, data); - setAxiosAuthHeader(data); - } -}; - -/** - * delete auth header in axios and clear localStorage after signing out - */ -export const onSignedOut = () => { - delete axios.defaults.headers.common.Authorization; - localStorage.clear(); -}; +// export const onAuthenticated = (response) => { +// if (response && response.data) { +// const { data } = response; + // data.jwtData = jwtDecode(data.access_token); -export const getUserProfileFromRemote = () => ( - axios.get(GET_USER_PROFILE_ENDPOINT) -); +// saveDataInLocal(LOCAL_STORAGE_KEY.AUTH, data); +// setAxiosAuthHeader(data); +// } +// }; -/** - * - * update the new user data in localStorage after succesfully update user profile - * @param {User} newUser new user instance - * - */ -export const onUserProfileChanged = (newUser) => { - saveDataInLocal(LOCAL_STORAGE_KEY.USER, newUser); -}; -const isRangeAPIs = (config) => { - if (config && config.baseURL) { - return config.baseURL !== SSO_BASE_URL; - } - return true; -}; +// const isRangeAPIs = (config) => { +// if (config && config.baseURL) { +// return config.baseURL !== SSO_BASE_URL; +// } +// return true; +// }; /** * @@ -166,31 +148,31 @@ const isRangeAPIs = (config) => { * @param {function} logout the logout action * */ -export const registerAxiosInterceptors = (logout) => { - axios.interceptors.request.use((c) => { - const config = { ...c }; - const makeRequest = async () => { - try { - if (isTokenExpired() && !config.isRetry && isRangeAPIs()) { - if (process.env.NODE_ENV !== 'production') console.log('Access token is expired. Trying to refresh it'); - config.isRetry = true; - const refreshToken = getRefreshTokenFromLocal(); - const response = await refreshAccessToken(refreshToken, config.isRetry); - onAuthenticated(response); - - const data = response && response.data; - const tokenType = data && data.token_type; - const accessToken = data && data.access_token; - config.headers.Authorization = tokenType && accessToken && `${tokenType} ${accessToken}`; - } - return config; - } catch (err) { - logout(); - if (process.env.NODE_ENV !== 'production') console.log('Refresh token is also expired. Signing out.'); - throw err; - } - }; - // return config; - return makeRequest(); - }); -}; +// export const registerAxiosInterceptors = (logout) => { +// axios.interceptors.request.use((c) => { +// const config = { ...c }; +// const makeRequest = async () => { +// try { +// if (isTokenExpired() && !config.isRetry && isRangeAPIs()) { +// if (process.env.NODE_ENV !== 'production') console.log('Access token is expired. Trying to refresh it'); +// config.isRetry = true; +// const refreshToken = getRefreshTokenFromLocal(); +// const response = await refreshAccessToken(refreshToken, config.isRetry); +// onAuthenticated(response); + +// const data = response && response.data; +// const tokenType = data && data.token_type; +// const accessToken = data && data.access_token; +// config.headers.Authorization = tokenType && accessToken && `${tokenType} ${accessToken}`; +// } +// return config; +// } catch (err) { +// logout(); +// if (process.env.NODE_ENV !== 'production') console.log('Refresh token is also expired. Signing out.'); +// throw err; +// } +// }; +// // return config; +// return makeRequest(); +// }); +// }; diff --git a/src/utils/index.js b/src/utils/index.js index 17cb057ca..b4ea736e9 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -25,12 +25,13 @@ import { STATUS500, } from '../constants/strings'; -export { default } from './axios'; +export { default as axios } from './axios'; export * from './calculation'; export * from './queryString'; export * from './format'; export * from './authentication'; export * from './localStorage'; +export * from './user'; export const getObjValues = obj => Object.keys(obj).map(e => obj[e]); /** diff --git a/src/utils/user.js b/src/utils/user.js new file mode 100644 index 000000000..c0a45ddc7 --- /dev/null +++ b/src/utils/user.js @@ -0,0 +1,33 @@ +import { USER_ROLE } from '../constants/variables'; + +export const getUserfullName = (user = {}) => ( + user.givenName && user.familyName && `${user.givenName} ${user.familyName}` +); + +export const isUserActive = (user = {}) => user.active; + +export const isUserAdmin = (user = {}) => ( + isUserActive(user) && user.roles && (user.roles.indexOf(USER_ROLE.ADMINISTRATOR) >= 0) +); + +export const isUserRangeOfficer = (user = {}) => ( + isUserActive(user) && user.roles && (user.roles.indexOf(USER_ROLE.RANGE_OFFICER) >= 0) +); + +export const isUserAgreementHolder = (user = {}) => ( + isUserActive(user) && user.roles && (user.roles.indexOf(USER_ROLE.AGREEMENT_HOLDER) >= 0) +); + +export const userHaveRole = (user = {}) => ( + isUserAdmin(user) || isUserRangeOfficer(user) || isUserAgreementHolder(user) +); + +export const getUserInitial = (user = {}) => { + let initial = 'NP'; + const { familyName, givenName } = user; + if (familyName && givenName && typeof familyName === 'string' && typeof givenName === 'string') { + initial = givenName.charAt(0) + familyName.charAt(0); + } + + return initial; +};