diff --git a/lib/actions/auth.js b/lib/actions/auth.js index cf5478e0f..b14e2e5a3 100644 --- a/lib/actions/auth.js +++ b/lib/actions/auth.js @@ -1,4 +1,4 @@ -import { push } from 'connected-react-router' +import { replace, push } from 'connected-react-router' import { setPathBeforeSignIn } from '../actions/user' @@ -30,30 +30,22 @@ export function showLoginError (err) { /** * This function is called by the Auth0Provider component, with the described parameter(s), * after the user signs in. - * @param {Object} appState The state that was stored when calling useAuth().login(). + * @param {Object} appState The state stored when calling useAuth0().loginWithRedirect + * or when instantiating a component that uses withAuhenticationRequired. */ export function processSignIn (appState) { return function (dispatch, getState) { - if (appState && appState.urlHash) { - // At this stage after login, Auth0 has already redirected to /signedin (Auth0-whitelisted) - // which shows the AfterLoginScreen. - // - // Here, we save the URL hash prior to login (contains a combination of itinerary search, stop/trip view, etc.), - // so that the AfterLoginScreen can redirect back there when logged-in user info is fetched. - // (For routing, it is easier to deal with the path without the hash sign.) - const hashIndex = appState.urlHash.indexOf('#') - const urlHashWithoutHash = hashIndex >= 0 - ? appState.urlHash.substr(hashIndex + 1) - : '/' - dispatch(setPathBeforeSignIn(urlHashWithoutHash)) - } else if (appState && appState.returnTo) { - // TODO: Handle other after-login situations. - // Note that when redirecting from a login-protected (e.g. account) page while logged out, - // then returnTo is set by Auth0 to this object format: - // { - // pathname: "/" - // query: { ... } - // } + if (appState && appState.returnTo) { + // Remove URL parameters that were added by auth0-react + // (see https://github.com/auth0/auth0-react/blob/adac2e810d4f6d33253cb8b2016fcedb98a3bc16/examples/cra-react-router/src/index.tsx#L7). + window.history.replaceState({}, '', window.location.pathname) + + // Here, we add the hash to the redux state (portion of the URL after '#' that contains the route/page name e.g. /account, + // and includes a combination of itinerary search, stop/trip view, etc.) that was passed to appState.returnTo prior to login. + // Once the redux state set, we redirect to the "/signedin" route (whitelisted in Auth0 dashboard), where the AfterLoginScreen + // will in turn fetch the user data then redirect the web browser back to appState.returnTo. + dispatch(setPathBeforeSignIn(appState.returnTo)) + dispatch(replace('/signedin')) } } } diff --git a/lib/actions/user.js b/lib/actions/user.js index 294d895d5..aa8e8d095 100644 --- a/lib/actions/user.js +++ b/lib/actions/user.js @@ -50,63 +50,64 @@ function getMiddlewareVariables (state) { } /** - * Fetches the saved/monitored trips for a user. - * We use the accessToken to fetch the data regardless of - * whether the process to populate state.user is completed or not. + * Attempts to fetch user preferences (or set initial values if the user is being created) + * into the redux state, under state.user. + * @param auth0 If provided, the auth0 login object used to initially obtain the auth0 access token + * for subsequent middleware fetches (and also to initialize new users from auth0 email and id). + * If absent, state.user.accessToken will be used for fetches. */ -export function fetchUserMonitoredTrips (accessToken) { +export function fetchOrInitializeUser (auth0) { return async function (dispatch, getState) { - const { apiBaseUrl, apiKey } = getMiddlewareVariables(getState()) - const requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}` + const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState()) + const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/fromtoken` - const { data: trips, status } = await secureFetch(requestUrl, accessToken, apiKey, 'GET') - if (status === 'success') { - dispatch(setCurrentUserMonitoredTrips(trips.data)) + // Get the Auth0 access token. If one is in state.user, use it, + // otherwise if auth0 is provided, fetch it. + let token + if (accessToken) { + token = accessToken + } else if (auth0) { + try { + token = await auth0.getAccessTokenSilently() + } catch (error) { + // TODO: improve UI if there is an errror. + alert('Error obtaining an authorization token.') + } } - } -} -/** - * Fetches user preferences to state.user, - * or set initial values under state.user if no user has been loaded. - */ -export function fetchOrInitializeUser (auth) { - return async function (dispatch, getState) { - const { apiBaseUrl, apiKey } = getMiddlewareVariables(getState()) - const { accessToken, user: authUser } = auth - const requestUrl = `${apiBaseUrl}${API_OTPUSER_PATH}/fromtoken` - - const { data: user, status } = await secureFetch(requestUrl, accessToken, apiKey) - - // Beware! On AWS API gateway, if a user is not found in the middleware - // (e.g. they just created their Auth0 password but have not completed the account setup form yet), - // the call above will return, for example: - // { - // status: 'success', - // data: { - // "result": "ERR", - // "message": "No user with id=000000 found.", - // "code": 404, - // "detail": null - // } - // } - // - // The same call to a middleware instance that is not behind an API gateway - // will return: - // { - // status: 'error', - // message: 'Error get-ing user...' - // } - // TODO: Improve AWS response. - - const isNewAccount = status === 'error' || (user && user.result === 'ERR') - if (!isNewAccount) { - // Load user's monitored trips before setting the user state. - await dispatch(fetchUserMonitoredTrips(accessToken)) - - dispatch(setCurrentUser({ accessToken, user })) - } else { - dispatch(setCurrentUser({ accessToken, user: createNewUser(authUser) })) + // Once accessToken is available, proceed to fetch or initialize loggedInUser. + if (token) { + const { data: user, status } = await secureFetch(requestUrl, token, apiKey) + + // Beware! On AWS API gateway, if a user is not found in the middleware + // (e.g. they just created their Auth0 password but have not completed the account setup form yet), + // the call above will return, for example: + // { + // status: 'success', + // data: { + // "result": "ERR", + // "message": "No user with id=000000 found.", + // "code": 404, + // "detail": null + // } + // } + // + // The same call to a middleware instance that is not behind an API gateway + // will return: + // { + // status: 'error', + // message: 'Error get-ing user...' + // } + // TODO: Improve AWS response. + + const isNewAccount = status === 'error' || (user && user.result === 'ERR') + const userData = isNewAccount ? createNewUser(auth0.user) : user + dispatch(setCurrentUser({ accessToken: token, user: userData })) + + // Also load monitored trips for existing users. + if (!isNewAccount) { + dispatch(fetchUserMonitoredTrips()) + } } } } @@ -151,6 +152,23 @@ export function createOrUpdateUser (userData, silentOnSuccess = false) { } } +/** + * Fetches the saved/monitored trips for a user. + * We use the accessToken to fetch the data regardless of + * whether the process to populate state.user is completed or not. + */ +export function fetchUserMonitoredTrips () { + return async function (dispatch, getState) { + const { accessToken, apiBaseUrl, apiKey } = getMiddlewareVariables(getState()) + const requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}` + + const { data: trips, status } = await secureFetch(requestUrl, accessToken, apiKey, 'GET') + if (status === 'success') { + dispatch(setCurrentUserMonitoredTrips(trips.data)) + } + } +} + /** * Updates a logged-in user's monitored trip, * then, if that was successful, alerts (optional) @@ -182,7 +200,7 @@ export function createOrUpdateUserMonitoredTrip (tripData, isNew, silentOnSucces } // Reload user's monitored trips after add/update. - await dispatch(fetchUserMonitoredTrips(accessToken)) + await dispatch(fetchUserMonitoredTrips()) // Finally, navigate to the saved trips page. dispatch(routeTo('/savedtrips')) @@ -204,7 +222,7 @@ export function deleteUserMonitoredTrip (tripId) { const { message, status } = await secureFetch(requestUrl, accessToken, apiKey, 'DELETE') if (status === 'success') { // Reload user's monitored trips after deletion. - await dispatch(fetchUserMonitoredTrips(accessToken)) + dispatch(fetchUserMonitoredTrips()) } else { alert(`An error was encountered:\n${JSON.stringify(message)}`) } @@ -237,8 +255,7 @@ export function requestPhoneVerificationSms (newPhoneNumber) { if (status === 'success') { // Refetch user and update application state with new phone number and verification status. - // (This also refetches the user's monitored trip, and that's ok.) - await dispatch(fetchOrInitializeUser({ accessToken })) + dispatch(fetchOrInitializeUser()) } else { alert(`An error was encountered:\n${JSON.stringify(message)}`) } @@ -264,8 +281,7 @@ export function verifyPhoneNumber (code) { if (status === 'success' && data) { if (data.status === 'approved') { // Refetch user and update application state with new phone number and verification status. - // (This also refetches the user's monitored trip, and that's ok.) - dispatch(fetchOrInitializeUser({ accessToken })) + dispatch(fetchOrInitializeUser()) } else { // Otherwise, the user entered a wrong/incorrect code. alert('The code you entered is invalid. Please try again.') diff --git a/lib/components/app/responsive-webapp.js b/lib/components/app/responsive-webapp.js index 49466492c..1ff952765 100644 --- a/lib/components/app/responsive-webapp.js +++ b/lib/components/app/responsive-webapp.js @@ -1,3 +1,4 @@ +import { Auth0Provider } from '@auth0/auth0-react' import { ConnectedRouter } from 'connected-react-router' import { createHashHistory } from 'history' import isEqual from 'lodash.isequal' @@ -6,7 +7,6 @@ import PropTypes from 'prop-types' import React, { Component } from 'react' import { connect } from 'react-redux' import { Route, Switch, withRouter } from 'react-router' -import { Auth0Provider } from 'use-auth0-hooks' import PrintLayout from './print-layout' import * as authActions from '../../actions/auth' diff --git a/lib/components/user/nav-login-button-auth0.js b/lib/components/user/nav-login-button-auth0.js index 415d30b98..9687bd37d 100644 --- a/lib/components/user/nav-login-button-auth0.js +++ b/lib/components/user/nav-login-button-auth0.js @@ -1,7 +1,8 @@ +import { useAuth0 } from '@auth0/auth0-react' import React from 'react' -import { useAuth } from 'use-auth0-hooks' import { URL_ROOT } from '../../util/constants' +import { getCurrentRoute } from '../../util/ui' import NavLoginButton from './nav-login-button' /** @@ -13,14 +14,15 @@ const NavLoginButtonAuth0 = ({ links, style }) => { - const { isAuthenticated, login, logout, user } = useAuth() + const { isAuthenticated, loginWithRedirect, logout, user } = useAuth0() // On login, preserve the current trip query if any. - // TODO: check that URLs are whitelisted. All trip query URLs in /#/ are. - const afterLoginPath = '/#/signedin' - const handleLogin = () => login({ - redirect_uri: `${URL_ROOT}${afterLoginPath}`, - appState: {urlHash: window.location.hash} // The part of href from #/?, e.g. #/?ui_activeSearch=... + const handleLogin = () => loginWithRedirect({ + appState: { returnTo: getCurrentRoute() } + }) + const handleLogout = () => logout({ + // Logout to the map with no search. + returnTo: URL_ROOT }) // On logout, it is better to "clear" the screen, so @@ -32,7 +34,7 @@ const NavLoginButtonAuth0 = ({ id={id} links={links} onSignInClick={handleLogin} - onSignOutClick={logout} + onSignOutClick={handleLogout} profile={isAuthenticated ? user : null} style={style} /> diff --git a/lib/components/user/saved-trip-list.js b/lib/components/user/saved-trip-list.js index 29b97da46..321e66705 100644 --- a/lib/components/user/saved-trip-list.js +++ b/lib/components/user/saved-trip-list.js @@ -1,12 +1,14 @@ +import { withAuthenticationRequired } from '@auth0/auth0-react' import clone from 'clone' import React, { Component } from 'react' import { Button, ButtonGroup, Glyphicon, Panel } from 'react-bootstrap' import { connect } from 'react-redux' -import { withLoginRequired } from 'use-auth0-hooks' import * as uiActions from '../../actions/ui' import * as userActions from '../../actions/user' import DesktopNav from '../app/desktop-nav' +import { RETURN_TO_CURRENT_ROUTE } from '../../util/ui' +import AwaitingScreen from './awaiting-screen' import LinkButton from './link-button' import TripSummaryPane from './trip-summary-pane' import withLoggedInUserSupport from './with-logged-in-user-support' @@ -19,7 +21,10 @@ const SavedTripList = ({ trips }) => { const accountLink =