Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
226 changes: 131 additions & 95 deletions lib/actions/user.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { createAction } from 'redux-actions'

import {
addTrip,
addUser,
deleteTrip,
fetchUser,
getTrips,
updateTrip,
updateUser
} from '../util/middleware'
import { secureFetch } from '../util/middleware'
import { isNewUser } from '../util/user'

// Middleware API paths.
const API_MONITORTRIP_PATH = '/api/secure/monitoredtrip'
const API_USER_PATH = '/api/secure/user'

const setCurrentUser = createAction('SET_CURRENT_USER')
const setCurrentUserMonitoredTrips = createAction('SET_CURRENT_USER_MONITORED_TRIPS')
export const setPathBeforeSignIn = createAction('SET_PATH_BEFORE_SIGNIN')
Expand All @@ -28,102 +24,137 @@ function createNewUser (auth0User) {
}

/**
* 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.
* Helper function that
* - extracts key variables from the state and passes them to the code to execute:
* - apiKey and apiBaseUrl from state.otp.config.persistence.otp_middleware,
* - accessToken and loggedIUnUser from state.user.
* - checks that otp_middleware is set, and throws an error if not.
* @param functionToExecute a function that can be waited upon,
* with parameters (dispatch, arguments), that contains the code to be
* executed if the OTP middleware is configured.
* @return a redux action for the code to be executed.
*/
export function fetchUserMonitoredTrips (accessToken) {
function executeWithMiddleware (functionToExecute) {
return async function (dispatch, getState) {
const { otp } = getState()
const { otp, user } = getState()
const { otp_middleware: otpMiddleware = null } = otp.config.persistence

if (otpMiddleware) {
const { data: trips, status: fetchStatus } = await getTrips(otpMiddleware, accessToken)
if (fetchStatus === 'success') {
dispatch(setCurrentUserMonitoredTrips(trips))
}
const { accessToken, loggedInUser } = user
const { apiBaseUrl, apiKey } = otpMiddleware
await functionToExecute(dispatch, {
accessToken,
apiBaseUrl,
apiKey,
loggedInUser
})
} else {
throw new Error('This action requires a valid middleware configuration.')
}
}
}

/**
* Fetches user preferences to state.user, or set initial values under state.user if no user has been loaded.
* 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 fetchOrInitializeUser (auth) {
return async function (dispatch, getState) {
const { otp } = getState()
const { otp_middleware: otpMiddleware = null } = otp.config.persistence
export function fetchUserMonitoredTrips (accessToken) {
return executeWithMiddleware(async (dispatch, { apiBaseUrl, apiKey }) => {
const requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}`

if (otpMiddleware) {
const { accessToken, user: authUser } = auth
const { data: user, status: fetchUserStatus } = await fetchUser(otpMiddleware, accessToken)

// 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 = fetchUserStatus === 'error' || (user && user.result === 'ERR')
if (!isNewAccount) {
// Load user's monitored trips before setting the user state.
await dispatch(fetchUserMonitoredTrips(accessToken))
const { data: trips, status } = await secureFetch(requestUrl, accessToken, apiKey, 'GET')
if (status === 'success') {
dispatch(setCurrentUserMonitoredTrips(trips))
}
})
}

dispatch(setCurrentUser({ accessToken, user }))
} else {
dispatch(setCurrentUser({ accessToken, user: createNewUser(authUser) }))
}
/**
* Fetches user preferences to state.user,
* or set initial values under state.user if no user has been loaded.
*/
export function fetchOrInitializeUser (auth) {
return executeWithMiddleware(async (dispatch, { apiBaseUrl, apiKey }) => {
const { accessToken, user: authUser } = auth
const requestUrl = `${apiBaseUrl}${API_USER_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) }))
}
}
})
}

/**
* Updates (or creates) a user entry in the middleware,
* then, if that was successful, updates the redux state with that user.
* @param userData the user entry to persist.
* @param silentOnSuccess true to suppress the confirmation if the operation is successful (e.g. immediately after user accepts the terms).
*/
export function createOrUpdateUser (userData) {
return async function (dispatch, getState) {
const { otp, user } = getState()
const { otp_middleware: otpMiddleware = null } = otp.config.persistence
export function createOrUpdateUser (userData, silentOnSuccess = false) {
return executeWithMiddleware(async (dispatch, { accessToken, apiBaseUrl, apiKey, loggedInUser }) => {
const { id } = userData // Middleware ID, NOT auth0 (or similar) id.
let requestUrl, method

// Determine URL and method to use.
if (isNewUser(loggedInUser)) {
requestUrl = `${apiBaseUrl}${API_USER_PATH}`
method = 'POST'
} else if (id) {
requestUrl = `${apiBaseUrl}${API_USER_PATH}/${id}`
method = 'PUT'
}

if (otpMiddleware) {
const { accessToken, loggedInUser } = user

let result
if (isNewUser(loggedInUser)) {
result = await addUser(otpMiddleware, accessToken, userData)
} else {
result = await updateUser(otpMiddleware, accessToken, userData)
}
if (requestUrl) {
const result = await secureFetch(requestUrl, accessToken, apiKey, method, {
body: JSON.stringify(userData)
})

// TODO: improve the UI feedback messages for this.
if (result.status === 'success' && result.data) {
alert('Your preferences have been saved.')
if (!silentOnSuccess) {
alert('Your preferences have been saved.')
}

// Update application state with the user entry as saved
// (as returned) by the middleware.
const userData = result.data
dispatch(setCurrentUser({ accessToken, user: userData }))
dispatch(setCurrentUser({ accessToken, user: result.data }))
} else {
alert(`An error was encountered:\n${JSON.stringify(result)}`)
}
} else {
alert('Corrupted state: User ID not available for exiting user.')
}
}
})
}

/**
Expand All @@ -132,19 +163,23 @@ export function createOrUpdateUser (userData) {
* with the updated trip.
*/
export function createOrUpdateUserMonitoredTrip (tripData, isNew) {
return async function (dispatch, getState) {
const { otp, user } = getState()
const { otp_middleware: otpMiddleware = null } = otp.config.persistence

if (otpMiddleware) {
const { accessToken } = user
return executeWithMiddleware(async (dispatch, { accessToken, apiBaseUrl, apiKey }) => {
const { id } = tripData
let requestUrl, method

// Determine URL and method to use.
if (isNew) {
requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}`
method = 'POST'
} else if (id) {
requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}/${id}`
method = 'PUT'
}

let result
if (isNew) {
result = await addTrip(otpMiddleware, accessToken, tripData)
} else {
result = await updateTrip(otpMiddleware, accessToken, tripData)
}
if (requestUrl) {
const result = await secureFetch(requestUrl, accessToken, apiKey, method, {
body: JSON.stringify(tripData)
})

// TODO: improve the UI feedback messages for this.
if (result.status === 'success' && result.data) {
Expand All @@ -155,29 +190,30 @@ export function createOrUpdateUserMonitoredTrip (tripData, isNew) {
} else {
alert(`An error was encountered:\n${JSON.stringify(result)}`)
}
} else {
alert('Corrupted state: Trip ID not available for exiting trip.')
}
}
})
}

/**
* Deletes a logged-in user's monitored trip,
* then, if that was successful, refreshes the redux monitoredTrips state.
*/
export function deleteUserMonitoredTrip (id) {
return async function (dispatch, getState) {
const { otp, user } = getState()
const { otp_middleware: otpMiddleware = null } = otp.config.persistence

if (otpMiddleware) {
const { accessToken } = user
const deleteResult = await deleteTrip(otpMiddleware, accessToken, id)
return executeWithMiddleware(async (dispatch, { accessToken, apiBaseUrl, apiKey }) => {
const requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}/${id}`

if (id) {
const deleteResult = secureFetch(requestUrl, accessToken, apiKey, 'DELETE')
if (deleteResult.status === 'success') {
// Reload user's monitored trips after deletion.
await dispatch(fetchUserMonitoredTrips(accessToken))
} else {
alert(`An error was encountered:\n${JSON.stringify(deleteResult)}`)
}
} else {
alert('Corrupted state: Monitored Trip ID not available for exiting user.')
}
}
})
}
84 changes: 0 additions & 84 deletions lib/util/middleware.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
if (typeof (fetch) === 'undefined') require('isomorphic-fetch')

const API_USER_PATH = '/api/secure/user'
const API_MONITORTRIP_PATH = '/api/secure/monitoredtrip'

/**
* This method builds the options object for call to the fetch method.
* @param {string} accessToken If non-null, a bearer Authorization header will be added with the specified token.
Expand Down Expand Up @@ -58,84 +55,3 @@ export async function secureFetch (url, accessToken, apiKey, method = 'get', opt
data: await res.json()
}
}

// TODO: Move methods below to user/entity-specific files?
export async function fetchUser (middlewareConfig, token) {
const { apiBaseUrl, apiKey } = middlewareConfig
const requestUrl = `${apiBaseUrl}${API_USER_PATH}/fromtoken`

return secureFetch(requestUrl, token, apiKey)
}

export async function addUser (middlewareConfig, token, data) {
const { apiBaseUrl, apiKey } = middlewareConfig
const requestUrl = `${apiBaseUrl}${API_USER_PATH}`

return secureFetch(requestUrl, token, apiKey, 'POST', {
body: JSON.stringify(data)
})
}

export async function updateUser (middlewareConfig, token, data) {
const { apiBaseUrl, apiKey } = middlewareConfig
const { id } = data // Middleware ID, NOT auth0 (or similar) id.
const requestUrl = `${apiBaseUrl}${API_USER_PATH}/${id}`

if (id) {
return secureFetch(requestUrl, token, apiKey, 'PUT', {
body: JSON.stringify(data)
})
} else {
return {
status: 'error',
message: 'Corrupted state: User ID not available for exiting user.'
}
}
}

export async function getTrips (middlewareConfig, token) {
const { apiBaseUrl, apiKey } = middlewareConfig
const requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}`

return secureFetch(requestUrl, token, apiKey, 'GET')
}

export async function addTrip (middlewareConfig, token, data) {
const { apiBaseUrl, apiKey } = middlewareConfig
const requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}`

return secureFetch(requestUrl, token, apiKey, 'POST', {
body: JSON.stringify(data)
})
}

export async function updateTrip (middlewareConfig, token, data) {
const { apiBaseUrl, apiKey } = middlewareConfig
const { id } = data
const requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}/${id}`

if (id) {
return secureFetch(requestUrl, token, apiKey, 'PUT', {
body: JSON.stringify(data)
})
} else {
return {
status: 'error',
message: 'Corrupted state: Monitored Trip ID not available for exiting user.'
}
}
}

export async function deleteTrip (middlewareConfig, token, id) {
const { apiBaseUrl, apiKey } = middlewareConfig
const requestUrl = `${apiBaseUrl}${API_MONITORTRIP_PATH}/${id}`

if (id) {
return secureFetch(requestUrl, token, apiKey, 'DELETE')
} else {
return {
status: 'error',
message: 'Corrupted state: Monitored Trip ID not available for exiting user.'
}
}
}