Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
dff0778
fix(auth0-react): Make login/logout work with auth0-react.
binh-dam-ibigroup Sep 18, 2020
411b0bb
refactor: Adapt rest of code to auth0-react.
binh-dam-ibigroup Sep 18, 2020
390f485
build(deps): Remove use-auth0-hooks.
binh-dam-ibigroup Sep 18, 2020
2c962c4
fix(login): Support login from protected URLs.
binh-dam-ibigroup Sep 21, 2020
6fd4e62
docs(actions/auth.js): Tweak comments.
binh-dam-ibigroup Sep 21, 2020
92edadb
refactor(util/constants): Reinstate URL_ROOT.
binh-dam-ibigroup Sep 22, 2020
4a6c291
refactor(util/ui): Extract methods for redirecting to current route.
binh-dam-ibigroup Sep 23, 2020
0eedf9d
refactor(WithLoggedInUserSupport): Consolidate condition for fetching…
binh-dam-ibigroup Sep 23, 2020
3ac1fee
Merge branch 'dev' into auth0-react
binh-dam-ibigroup Sep 23, 2020
1fa9713
refactor(WithLoggedInUserSupport): Rename shouldFetchUser.
binh-dam-ibigroup Sep 24, 2020
5f0274a
Merge branch 'dev' into auth0-react
binh-dam-ibigroup Sep 24, 2020
c9bb706
refactor(withLoggedInUserSupport): Address PR comments
binh-dam-ibigroup Oct 9, 2020
8fe223e
refactor(WithLoggedInUserSupport): Stop passing auth0 to children.
binh-dam-ibigroup Oct 9, 2020
842ad9b
Merge branch 'dev' into auth0-react
binh-dam-ibigroup Oct 9, 2020
e25ef5d
refactor(actions/user): Store auth0.user in redux state.
binh-dam-ibigroup Oct 21, 2020
3eee512
revert: Revert commits e25ef5d, 8fe223e
binh-dam-ibigroup Oct 21, 2020
67f8807
refactor(createUserReducer): Add initial user redux state.
binh-dam-ibigroup Oct 22, 2020
d359a8b
refactor(WithLoggedInUserSupport): Remove renderChidrenWithProps in b…
binh-dam-ibigroup Oct 22, 2020
99755e5
refactor(actions/user): Add method to ensure user's trips are loaded.
binh-dam-ibigroup Oct 22, 2020
dc1aabf
Merge branch 'dev' into auth0-react
binh-dam-ibigroup Oct 22, 2020
13d7936
fix(actions/user): Fix response with pagination.
binh-dam-ibigroup Oct 23, 2020
166d00f
refactor(actions/user): Support async load of trips after loading use…
binh-dam-ibigroup Oct 27, 2020
9b74a57
refactor(Remove unused arg, tweak comment.):
binh-dam-ibigroup Oct 28, 2020
d2e4caa
Merge branch 'dev' into auth0-react
binh-dam-ibigroup Oct 28, 2020
b27eb18
refactor: Resolve remaining merge issues from PR #224.
binh-dam-ibigroup Oct 28, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 14 additions & 22 deletions lib/actions/auth.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { push } from 'connected-react-router'
import { replace, push } from 'connected-react-router'

import { setPathBeforeSignIn } from '../actions/user'

Expand Down Expand Up @@ -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'))
}
}
}
132 changes: 74 additions & 58 deletions lib/actions/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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'))
Expand All @@ -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)}`)
}
Expand Down Expand Up @@ -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)}`)
}
Expand All @@ -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.')
Expand Down
2 changes: 1 addition & 1 deletion lib/components/app/responsive-webapp.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'
Expand Down
18 changes: 10 additions & 8 deletions lib/components/user/nav-login-button-auth0.js
Original file line number Diff line number Diff line change
@@ -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'

/**
Expand All @@ -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
Expand All @@ -32,7 +34,7 @@ const NavLoginButtonAuth0 = ({
id={id}
links={links}
onSignInClick={handleLogin}
onSignOutClick={logout}
onSignOutClick={handleLogout}
profile={isAuthenticated ? user : null}
style={style}
/>
Expand Down
16 changes: 12 additions & 4 deletions lib/components/user/saved-trip-list.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -19,7 +21,10 @@ const SavedTripList = ({ trips }) => {
const accountLink = <p><LinkButton to='/account'>Back to My Account</LinkButton></p>
let content

if (!trips || trips.length === 0) {
if (!trips) {
// Flash an indication while user trips are being loaded.
content = <AwaitingScreen />
} else if (trips.length === 0) {
content = (
<>
{accountLink}
Expand All @@ -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}
Expand Down Expand Up @@ -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
)

Expand Down
Loading