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 =

Back to My Account

let content - if (!trips || trips.length === 0) { + if (!trips) { + // Flash an indication while user trips are being loaded. + content = + } else if (trips.length === 0) { content = ( <> {accountLink} @@ -28,7 +33,7 @@ const SavedTripList = ({ trips }) => { ) } else { - // Stack the saved trip summaries. When the user clicks on one, they can edit that trip. + // Stack the saved trip summaries and commands. content = ( <> {accountLink} @@ -60,7 +65,10 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = {} export default withLoggedInUserSupport( - withLoginRequired(connect(mapStateToProps, mapDispatchToProps)(SavedTripList)), + withAuthenticationRequired( + connect(mapStateToProps, mapDispatchToProps)(SavedTripList), + RETURN_TO_CURRENT_ROUTE + ), true ) diff --git a/lib/components/user/saved-trip-screen.js b/lib/components/user/saved-trip-screen.js index a8b6bed2c..1c8f40cc5 100644 --- a/lib/components/user/saved-trip-screen.js +++ b/lib/components/user/saved-trip-screen.js @@ -1,19 +1,21 @@ +import { withAuthenticationRequired } from '@auth0/auth0-react' import clone from 'clone' import { Form, Formik } from 'formik' import React, { Component } from 'react' import { connect } from 'react-redux' -import { withLoginRequired } from 'use-auth0-hooks' import * as yup from 'yup' import * as uiActions from '../../actions/ui' import * as userActions from '../../actions/user' import DesktopNav from '../app/desktop-nav' +import AwaitingScreen from './awaiting-screen' import SavedTripEditor from './saved-trip-editor' import TripBasicsPane from './trip-basics-pane' import TripNotificationsPane from './trip-notifications-pane' import TripSummaryPane from './trip-summary-pane' import { ALL_DAYS, arrayToDayFields, WEEKDAYS } from '../../util/monitored-trip' import { getActiveItineraries, getActiveSearch } from '../../util/state' +import { RETURN_TO_CURRENT_ROUTE } from '../../util/ui' import withLoggedInUserSupport from './with-logged-in-user-support' // The validation schema shape for the form fields. @@ -102,7 +104,7 @@ class SavedTripScreen extends Component { summary: TripSummaryPane } - componentDidMount () { + async componentDidMount () { const { isCreating, monitoredTrips } = this.props // There is a middleware limit of 5 saved trips, @@ -140,21 +142,24 @@ class SavedTripScreen extends Component { render () { const { isCreating, loggedInUser, monitoredTrips } = this.props - const monitoredTrip = this._getTripToEdit(this.props) - const otherTripNames = monitoredTrips - .filter(trip => trip !== monitoredTrip) - .map(trip => trip.tripName) - const clonedSchemaShape = clone(validationSchemaShape) - clonedSchemaShape.tripName = yup.string() - .required('Please enter a trip name') - .notOneOf(otherTripNames, 'You already using this name for another saved trip. Please enter a different name.') - const validationSchema = yup.object(clonedSchemaShape) - - return ( -
- {/* TODO: Do mobile view. */} - + let screenContents + if (!monitoredTrips) { + // Flash an indication while user trips are being loaded. + screenContents = + } else { + const monitoredTrip = this._getTripToEdit(this.props) + const otherTripNames = monitoredTrips + .filter(trip => trip !== monitoredTrip) + .map(trip => trip.tripName) + + const clonedSchemaShape = clone(validationSchemaShape) + clonedSchemaShape.tripName = yup.string() + .required('Please enter a trip name') + .notOneOf(otherTripNames, 'You already using this name for another saved trip. Please enter a different name.') + const validationSchema = yup.object(clonedSchemaShape) + + screenContents = ( + ) + } + + return ( +
+ {/* TODO: Do mobile view. */} + + {screenContents}
) } @@ -210,6 +223,9 @@ const mapDispatchToProps = { } export default withLoggedInUserSupport( - withLoginRequired(connect(mapStateToProps, mapDispatchToProps)(SavedTripScreen)), + withAuthenticationRequired( + connect(mapStateToProps, mapDispatchToProps)(SavedTripScreen), + RETURN_TO_CURRENT_ROUTE + ), true ) diff --git a/lib/components/user/user-account-screen.js b/lib/components/user/user-account-screen.js index d11bcc001..1ca183e98 100644 --- a/lib/components/user/user-account-screen.js +++ b/lib/components/user/user-account-screen.js @@ -1,12 +1,13 @@ +import { withAuthenticationRequired } from '@auth0/auth0-react' import clone from 'clone' import { Form, Formik } from 'formik' import React, { Component } from 'react' import { connect } from 'react-redux' -import { withLoginRequired } from 'use-auth0-hooks' import * as yup from 'yup' import * as uiActions from '../../actions/ui' import * as userActions from '../../actions/user' +import { RETURN_TO_CURRENT_ROUTE } from '../../util/ui' import { isNewUser } from '../../util/user' import DesktopNav from '../app/desktop-nav' import AccountSetupFinishPane from './account-setup-finish-pane' @@ -86,7 +87,9 @@ class UserAccountScreen extends Component { // In userData.savedLocations, filter out entries with blank addresses. const newUserData = clone(userData) - newUserData.savedLocations = newUserData.savedLocations.filter(({ address }) => address && address.length) + newUserData.savedLocations = newUserData.savedLocations.filter( + ({ address }) => address && address.length + ) await this.props.createOrUpdateUser(newUserData, silentOnSucceed) // TODO: Handle UI feedback (currently an alert() dialog inside createOrUpdateUser). @@ -129,7 +132,13 @@ class UserAccountScreen extends Component { // TODO: Update title bar during componentDidMount. render () { - const { auth, loggedInUser, phoneFormatOptions, requestPhoneVerificationSms, verifyPhoneNumber } = this.props + const { + auth0, + loggedInUser, + phoneFormatOptions, + requestPhoneVerificationSms, + verifyPhoneNumber + } = this.props return (
@@ -155,7 +164,7 @@ class UserAccountScreen extends Component { let formContents let DisplayComponent if (this.state.isNewUser) { - if (!auth.user.email_verified) { + if (!auth0.user.email_verified) { // Check and prompt for email verification first to avoid extra user wait. formContents = } else { @@ -212,6 +221,9 @@ const mapDispatchToProps = { } export default withLoggedInUserSupport( - withLoginRequired(connect(mapStateToProps, mapDispatchToProps)(UserAccountScreen)), + withAuthenticationRequired( + connect(mapStateToProps, mapDispatchToProps)(UserAccountScreen), + RETURN_TO_CURRENT_ROUTE + ), true ) diff --git a/lib/components/user/with-logged-in-user-support.js b/lib/components/user/with-logged-in-user-support.js index 4b60ee4d7..951c3f743 100644 --- a/lib/components/user/with-logged-in-user-support.js +++ b/lib/components/user/with-logged-in-user-support.js @@ -1,10 +1,9 @@ +import { withAuth0 } from '@auth0/auth0-react' import React, { Component } from 'react' import { connect } from 'react-redux' -import { withAuth } from 'use-auth0-hooks' import * as userActions from '../../actions/user' import { AUTH0_AUDIENCE, AUTH0_SCOPE } from '../../util/constants' -import { renderChildrenWithProps } from '../../util/ui' import AwaitingScreen from './awaiting-screen' /** @@ -25,42 +24,53 @@ import AwaitingScreen from './awaiting-screen' */ export default function withLoggedInUserSupport (WrappedComponent, requireLoggedInUser) { return props => ( - - - + ) } /** * This component ensures that values under state.user are set when a user is logged in. - * If needed by the children, this component displays a wait screen while state.user values are being fetched. - * Upon completion (or if no user is logged in or if auth is disabled), it renders children. + * If needed by the child WrappedComponent, this component displays a wait screen while state.user values are being fetched. + * Upon completion (or if no user is logged in or if auth is disabled), it renders the + * specified WrappedComponent with the passed props and the auth0 props if available. */ class UserLoaderScreen extends Component { + /** + * Determines whether user data should be fetched. + * @returns true if the logged-in user has passed Auth0 authentication + * and state.user.loggedInUser has not been set; false otherwise. + */ + loggedInUserIsUnfetched = () => { + const { auth0, loggedInUser } = this.props + return auth0 && auth0.isAuthenticated && !loggedInUser + } + componentDidUpdate () { - const { auth, fetchOrInitializeUser, loggedInUser } = this.props + const { auth0, fetchOrInitializeUser } = this.props - // Once accessToken is available, proceed to fetch or initialize loggedInUser. - if (auth && auth.accessToken && !loggedInUser) { - fetchOrInitializeUser(auth) + if (this.loggedInUserIsUnfetched()) { + fetchOrInitializeUser(auth0) } } render () { - const { auth, children, loggedInUser, requireLoggedInUser } = this.props + const { auth0, passedProps, requireLoggedInUser, WrappedComponent } = this.props - if (auth) { - if (requireLoggedInUser && auth.isAuthenticated && !loggedInUser) { - // Display a hint while fetching user data for logged in user (from componentDidMount). - // Don't display this if loggedInUser is already available. - // TODO: Improve this screen. - return - } else { - return renderChildrenWithProps(children, { auth }) - } + if (requireLoggedInUser && this.loggedInUserIsUnfetched()) { + // If a logged-in user is required, then + // display a hint while the logged-in user data is being fetched (from componentDidMount). + // Don't display this if loggedInUser is not required or is already available. + // TODO: Improve this screen. + return } - return children + // Forward the auth0 object to the wrapped component. + // (if there is no user, auth0 will be null and not be forwarded.) + return } } @@ -76,7 +86,7 @@ const mapDispatchToProps = { fetchOrInitializeUser: userActions.fetchOrInitializeUser } -const UserLoaderScreenWithAuth = withAuth( +const UserLoaderScreenWithAuth = withAuth0( connect(mapStateToProps, mapDispatchToProps)(UserLoaderScreen), { audience: AUTH0_AUDIENCE, diff --git a/lib/reducers/create-user-reducer.js b/lib/reducers/create-user-reducer.js index 952adc120..eb5a6b430 100644 --- a/lib/reducers/create-user-reducer.js +++ b/lib/reducers/create-user-reducer.js @@ -3,11 +3,15 @@ import update from 'immutability-helper' // TODO: port user-specific code from the otp reducer. function createUserReducer () { const initialState = { + accessToken: null, lastPhoneSmsRequest: { number: null, status: null, timestamp: new Date(0) - } + }, + loggedInUser: null, + loggedInUserMonitoredTrips: null, + pathBeforeSignIn: null } return (state = initialState, action) => { diff --git a/lib/util/constants.js b/lib/util/constants.js index 3f16df29e..9563256ae 100644 --- a/lib/util/constants.js +++ b/lib/util/constants.js @@ -4,4 +4,5 @@ export const DEFAULT_APP_TITLE = 'OpenTripPlanner' export const PERSISTENCE_STRATEGY_OTP_MIDDLEWARE = 'otp_middleware' // Gets the root URL, e.g. https://otp-instance.example.com:8080, computed once for all. +// TODO: support root URLs that involve paths or subfolders, as in https://otp-ui.example.com/path-to-ui/ export const URL_ROOT = `${window.location.protocol}//${window.location.host}` diff --git a/lib/util/ui.js b/lib/util/ui.js index 5231c2a80..58e4e1e1b 100644 --- a/lib/util/ui.js +++ b/lib/util/ui.js @@ -1,5 +1,3 @@ -import { Children, isValidElement, cloneElement } from 'react' - /** * @param {*} string the string to test. * @returns true if the string is null or of zero length. @@ -9,20 +7,19 @@ export function isBlank (string) { } /** - * Renders children with additional props. - * Modified from - * https://stackoverflow.com/questions/32370994/how-to-pass-props-to-this-props-children#32371612 - * @param children the child elements to modify. - * @param newProps the props to add. + * Returns the route that is in place before a user accesses the login page, + * e.g. if the URL is http://www.example.com/path/#/route?param=value, + * only /route?param=value is returned. A blank string is returned at the minimum per substr() function. */ -export function renderChildrenWithProps (children, newProps) { - const childrenWithProps = Children.map(children, child => { - // Checking isValidElement is the safe way and avoids a TS error too. - if (isValidElement(child)) { - return cloneElement(child, { ...newProps }) - } - return child - }) +export function getCurrentRoute () { + return window.location.hash.substr(1) +} - return childrenWithProps +/** + * Used in several components instantiated with auth0-react's withAuthenticationRequired(), + * so that the browser returns to the route (hash) in place before the user accessed the login page, + * e.g. /account/?ui_activeSearch=... without #. + */ +export const RETURN_TO_CURRENT_ROUTE = { + returnTo: getCurrentRoute } diff --git a/package.json b/package.json index 208c071d9..597e4172b 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ }, "homepage": "https://github.com/opentripplanner/otp-react-redux#readme", "dependencies": { + "@auth0/auth0-react": "^1.1.0", "@opentripplanner/base-map": "^1.0.2", "@opentripplanner/core-utils": "^2.1.2", "@opentripplanner/endpoints-overlay": "^1.0.2", @@ -89,7 +90,6 @@ "redux-logger": "^2.7.4", "redux-thunk": "^2.3.0", "transitive-js": "^0.13.3", - "use-auth0-hooks": "^0.7.0", "velocity-react": "^1.3.3", "yup": "^0.29.3" }, diff --git a/yarn.lock b/yarn.lock index a5ffb79e9..29c7785ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,16 +2,23 @@ # yarn lockfile v1 -"@auth0/auth0-spa-js@^1.2.4": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@auth0/auth0-spa-js/-/auth0-spa-js-1.7.0.tgz#4d19a7271bb031da4f696a2209d4bfe03dc857dd" - integrity sha512-OAeNUI0bTBdk3cIWJMBLgf4B1DvyPXmbBBDQI7EpGR3pU0/+9d19sDCPC1b9epp6Aa7pnd4OR5u9JIQCc3eSKw== +"@auth0/auth0-react@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@auth0/auth0-react/-/auth0-react-1.1.0.tgz#deeb5f1872f7c0d813cdb9115dfdf0a70a34e1c2" + integrity sha512-FqRzdSkraM2GFOl8NhdlpocALxCd9oNRxiPpIV0Lm5ophHUQRXaE8raTGjnPhHleJGJMw9XoEJ3ye9mjbkvweQ== + dependencies: + "@auth0/auth0-spa-js" "^1.12.1" + +"@auth0/auth0-spa-js@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@auth0/auth0-spa-js/-/auth0-spa-js-1.12.1.tgz#791cdda722afa25f4c8879b93b8d7ab54a26e4c1" + integrity sha512-YdXtA1T2wK4iNG79VlGS/CPfNNezS1nqZURerj71jKSf8ICVKmvJgUNXFEZEwNNU/KFdCCPCHd5wMeWLDyEILw== dependencies: - abortcontroller-polyfill "^1.4.0" - browser-tabs-lock "^1.2.8" - core-js "^3.6.4" + abortcontroller-polyfill "^1.5.0" + browser-tabs-lock "^1.2.9" + core-js "^3.6.5" es-cookie "^1.3.2" - fast-text-encoding "^1.0.1" + fast-text-encoding "^1.0.3" promise-polyfill "^8.1.3" unfetch "^4.1.0" @@ -2120,10 +2127,10 @@ abbrev@1, abbrev@~1.1.1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== -abortcontroller-polyfill@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.4.0.tgz#0d5eb58e522a461774af8086414f68e1dda7a6c4" - integrity sha512-3ZFfCRfDzx3GFjO6RAkYx81lPGpUS20ISxux9gLxuKnqafNcFQo59+IoZqpO2WvQlyc287B62HDnDdNYRmlvWA== +abortcontroller-polyfill@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.5.0.tgz#2c562f530869abbcf88d949a2b60d1d402e87a7c" + integrity sha512-O6Xk757Jb4o0LMzMOMdWvxpHWrQzruYBaUruFaIOfAQRnWFxfdXYobw12jrVHGtoXk6WiiyYzc0QWN9aL62HQA== accounting@^0.4.1: version "0.4.1" @@ -3796,10 +3803,12 @@ browser-resolve@^1.11.0, browser-resolve@^1.11.3, browser-resolve@^1.7.0: dependencies: resolve "1.1.7" -browser-tabs-lock@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/browser-tabs-lock/-/browser-tabs-lock-1.2.8.tgz#5642cad86df41111065a44c3ba744ce19b2af72f" - integrity sha512-Xrj33YUTltPDoGrD1KnaAn5ZuxnnlJFcIW9srVTPHbMNPd9MlcnBCWaGV0STlvGKu8Ok0ad5qxyx5sIwFTr/Ig== +browser-tabs-lock@^1.2.9: + version "1.2.11" + resolved "https://registry.yarnpkg.com/browser-tabs-lock/-/browser-tabs-lock-1.2.11.tgz#59d72c6653b827685b54fca1c0e6eb4aee08ef52" + integrity sha512-R0xXMzQ8CU0v52zSFn3EDGIfsjteMFJ6oIhhZK6+Vhz5NYzSxCP2epLD9PN7e2HprQl2QTReFptwp6c9tdOF0g== + dependencies: + lodash ">=4.17.19" browserify-aes@^1.0.0, browserify-aes@^1.0.4: version "1.2.0" @@ -4980,7 +4989,7 @@ core-js@^1.0.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= -core-js@^3.1.4, core-js@^3.6.4: +core-js@^3.1.4, core-js@^3.6.5: version "3.6.5" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== @@ -6778,10 +6787,10 @@ fast-levenshtein@~2.0.4: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= -fast-text-encoding@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.2.tgz#ff1ad5677bde049e0f8656aa6083a7ef2c5836e2" - integrity sha512-5rQdinSsycpzvAoHga2EDn+LRX1d5xLFsuNG0Kg61JrAT/tASXcLL0nf/33v+sAxlQcfYmWbTURa1mmAf55jGw== +fast-text-encoding@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53" + integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig== fastparse@^1.1.1: version "1.1.2" @@ -9911,7 +9920,7 @@ lodash@4.17.14: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== -lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: +lodash@>=4.17.19, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -13219,7 +13228,7 @@ query-string@^4.1.0, query-string@^4.2.3: object-assign "^4.1.0" strict-uri-encode "^1.0.0" -query-string@^6.8.2, query-string@^6.8.3: +query-string@^6.8.2: version "6.13.1" resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.1.tgz#d913ccfce3b4b3a713989fe6d39466d92e71ccad" integrity sha512-RfoButmcK+yCta1+FuU8REvisx1oEzhMKwhLUNcepQTPGcNMp1sIqjnfCtfnvGSQZQEhaBHvccujtWoUV3TTbA== @@ -16334,14 +16343,6 @@ url@^0.11.0, url@~0.11.0: punycode "1.3.2" querystring "0.2.0" -use-auth0-hooks@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/use-auth0-hooks/-/use-auth0-hooks-0.7.0.tgz#879a930ee36b893c91616ffa9b609ed8f685ff57" - integrity sha512-vL+G4jDu0nQduaMwg1+TMdLORpMgMQx72iPq0LGiEw0tXPmLmuIwi/7e7yrGJu+LuwZfD3g1qbI6Pjb+bDV7nw== - dependencies: - "@auth0/auth0-spa-js" "^1.2.4" - query-string "^6.8.3" - use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"