From c11f1b1a64392b23189f6076c984741b41ecbbc2 Mon Sep 17 00:00:00 2001 From: "Andrew Revinsky (DART)" Date: Tue, 15 Mar 2016 04:17:55 +0300 Subject: [PATCH] Authentication & user data retrieval fixed, API methods provided --- js-frontend/gulpfile.babel.js | 10 ++- js-frontend/src/App.js | 4 +- js-frontend/src/actions/authenticate.js | 72 +++++++++++++++++++ js-frontend/src/actions/configure.js | 68 ++---------------- js-frontend/src/actions/signIn.js | 30 ++++---- js-frontend/src/components/AuthComponent.js | 40 +++++++---- js-frontend/src/components/HeaderLinks.js | 3 +- .../src/controls/bootstrap/EmailSignUpForm.js | 11 +-- js-frontend/src/controls/bootstrap/Input.js | 3 +- .../src/entities/formToPayloadMappers.js | 4 +- js-frontend/src/reducers/auth/user.js | 9 +-- js-frontend/src/utils/api.js | 52 +++++++++++++- js-frontend/src/utils/clientSettings.js | 41 ++++++----- js-frontend/src/utils/constants.js | 3 +- js-frontend/src/utils/handleFetchResponse.js | 20 +++++- js-frontend/src/utils/sessionStorage.js | 24 ++++++- js-frontend/src/views/MyAccounts.js | 29 ++++++-- js-frontend/src/views/SignIn.js | 20 +++--- js-frontend/src/views/SignUp.js | 2 +- 19 files changed, 291 insertions(+), 154 deletions(-) diff --git a/js-frontend/gulpfile.babel.js b/js-frontend/gulpfile.babel.js index 7495944..7b07872 100644 --- a/js-frontend/gulpfile.babel.js +++ b/js-frontend/gulpfile.babel.js @@ -89,13 +89,21 @@ gulp.task('serve:start', ['serve:static'], () => { //} proxy: { + '/user' : { + target: 'http://localhost:8080' + }, '/login' : { target: 'http://localhost:8080' }, '/customers' : { target: 'http://localhost:8080' + }, + '/accounts' : { + target: 'http://localhost:8080' + }, + '/transfers' : { + target: 'http://localhost:8080' } - } }) .listen(PORT, '0.0.0.0', (err) => { diff --git a/js-frontend/src/App.js b/js-frontend/src/App.js index 5e1f0f3..79e4703 100644 --- a/js-frontend/src/App.js +++ b/js-frontend/src/App.js @@ -88,7 +88,9 @@ export function initialize({cookies, isServer, currentLocation, userAgent} = {}) default: { //apiUrl: '/', emailSignInPath: '/login', - emailRegistrationPath: '/customers' + emailRegistrationPath: '/customers', + currentUserPath: '/user', + accountsPath: '/accounts' } } ], { diff --git a/js-frontend/src/actions/authenticate.js b/js-frontend/src/actions/authenticate.js index 00cdaa8..e86385f 100644 --- a/js-frontend/src/actions/authenticate.js +++ b/js-frontend/src/actions/authenticate.js @@ -3,6 +3,27 @@ */ import T from '../constants/ACTION_TYPES'; +import { + getCurrentSettings, + setCurrentSettings, + getInitialEndpointKey, + setDefaultEndpointKey, + setCurrentEndpoint, + setCurrentEndpointKey, + retrieveData, + persistData, + destroySession, + persistUserData, + retrieveUserData, + retrieveHeaders +} from "../utils/sessionStorage"; + +import { + apiGetCurrentUser +} from '../utils/api'; + +import {entityReceived } from './entities'; + export function authenticateStart() { return { type: T.AUTH.AUTHENTICATE_START }; } @@ -12,3 +33,54 @@ export function authenticateComplete(user) { export function authenticateError(errors) { return { type: T.AUTH.AUTHENTICATE_ERROR, errors }; } + + +export function authenticate() { + return dispatch => { + + dispatch(authenticateStart()); + + const savedUserPromise = new Promise((rs, rj) => { + + const currentHeaders = retrieveHeaders(); + const accessToken = currentHeaders["access-token"]; + + if (!accessToken) { + return rj({ reason: 'no token'}); + } + + const savedUser = retrieveUserData(); + + if (savedUser) { + return rs(savedUser); + } + + return apiGetCurrentUser().then((userData) => { + persistUserData(userData); + dispatch(entityReceived(userData.id, userData)); + rs(userData); + }, (err) => { + debugger; + rj(err); + }); + + }); + + + return savedUserPromise + .then(user => { + + dispatch(authenticateComplete(user)); + + return user; + }) + .catch(({reason} = {}) => { + + dispatch(authenticateError([reason])); + + return Promise.resolve({reason}); + }); + + + }; +} \ No newline at end of file diff --git a/js-frontend/src/actions/configure.js b/js-frontend/src/actions/configure.js index b59751a..3ca9a9a 100644 --- a/js-frontend/src/actions/configure.js +++ b/js-frontend/src/actions/configure.js @@ -3,6 +3,7 @@ */ import * as C from "../utils/constants"; import { + authenticate, authenticateStart, authenticateComplete, authenticateError @@ -22,9 +23,8 @@ import {applyConfig} from "../utils/clientSettings"; // showPasswordResetErrorModal //} from "./ui"; -import {destroySession} from "../utils/sessionStorage"; import getRedirectInfo from "../utils/parseUrl"; -import {pushState} from "redux-router"; +import { pushState } from "redux-router"; import root from '../utils/root'; export const SET_ENDPOINT_KEYS = "SET_ENDPOINT_KEYS"; @@ -42,68 +42,10 @@ export function configure(endpoint={}, settings={}) { return dispatch => { - // don't render anything for OAuth redirects - if (settings.currentLocation && settings.currentLocation.match(/blank=true/)) { - return Promise.resolve({blank: true}); - } - dispatch(authenticateStart()); - - let promise, - firstTimeLogin, - mustResetPassword, - user, - headers; - - //let { authRedirectPath, authRedirectHeaders} = getRedirectInfo(root.location); - - // TODO: FiX! - //if (authRedirectPath) { - // dispatch(pushState(null, authRedirectPath)); - //} - - const currentHeaders = retrieveData(C.SAVED_CREDS_KEY) || {}; - - //if (authRedirectHeaders && authRedirectHeaders["access-token"]) { - if (currentHeaders && currentHeaders["access-token"]) { - - //settings.initialCredentials = { - // ...(settings.initialCredentials || {}), - // ...authRedirectHeaders, - // ...currentHeaders - //}; - } else { - destroySession(); - } - - // if tokens were invalidated by server, make sure to clear browser - // credentials - //if (!settings.initialCredentials) { - // destroySession(); - //} - - promise = Promise.resolve(applyConfig({ dispatch, endpoint, settings })); - - return promise - .then(user => { - - dispatch(authenticateComplete(user)); - - return user; - }) - .catch(({reason} = {}) => { - - dispatch(authenticateError([reason])); - - //if (firstTimeLogin) { - // dispatch(showFirstTimeLoginErrorModal()); - //} - // - //if (mustResetPassword) { - // dispatch(showPasswordResetErrorModal()); - //} - - return Promise.resolve({reason}); + return applyConfig({ dispatch, endpoint, settings }) + .then(() => { + return dispatch(authenticate()); }); }; diff --git a/js-frontend/src/actions/signIn.js b/js-frontend/src/actions/signIn.js index 516a93f..e4916ce 100644 --- a/js-frontend/src/actions/signIn.js +++ b/js-frontend/src/actions/signIn.js @@ -3,7 +3,8 @@ */ import { setCurrentEndpointKey, - getCurrentEndpointKey + getCurrentEndpointKey, + persistUserData } from "../utils/sessionStorage"; import { entityReceived } from './entities'; @@ -36,39 +37,34 @@ export function emailSignInError(errors) { export function emailSignIn(body) { return dispatch => { // save previous endpoint key in case of failure - var prevEndpointKey = getCurrentEndpointKey(); + //var prevEndpointKey = getCurrentEndpointKey(); const endpointKey = 'default'; // necessary for fetch to recognize the response as an api request - setCurrentEndpointKey(endpointKey); - var currentEndpointKey = getCurrentEndpointKey(); - - dispatch(storeCurrentEndpointKey(currentEndpointKey)); + //setCurrentEndpointKey(endpointKey); + //var currentEndpointKey = getCurrentEndpointKey(); + // + //dispatch(storeCurrentEndpointKey(currentEndpointKey)); dispatch(emailSignInStart()); return apiSignIn(body) .then(function(data = {}) { - const { id, customerInfo } = data; - if (id && customerInfo) { - const user = { - ...customerInfo, - uid: id - }; - debugger; - dispatch(entityReceived(id, user)); - return user; + const { id } = data; + if (id ) { + dispatch(entityReceived(id, data)); } return data; }) .then((user) => { + persistUserData(user); dispatch(emailSignInComplete(user)); }) .catch((errors) => { // revert endpoint key to what it was before failed request - setCurrentEndpointKey(prevEndpointKey); - dispatch(storeCurrentEndpointKey(prevEndpointKey)); + //setCurrentEndpointKey(prevEndpointKey); + //dispatch(storeCurrentEndpointKey(prevEndpointKey)); return dispatch(emailSignInError(errors)); }); }; diff --git a/js-frontend/src/components/AuthComponent.js b/js-frontend/src/components/AuthComponent.js index 76d72fb..ecea095 100644 --- a/js-frontend/src/components/AuthComponent.js +++ b/js-frontend/src/components/AuthComponent.js @@ -9,25 +9,37 @@ export function requireAuthentication(Component) { class AuthComponent extends React.Component { - componentWillMount() { - if (!this.props.isAuthenticated) { - - // redirect to login and add next param so we can redirect again after login - const redirectAfterLogin = this.props.location.pathname; - this.props.dispatch(pushState(null, `/signin?next=${redirectAfterLogin}`)); + checkRedirect(props) { + if (!props.isAuthenticated) { + // redirect to login and add next param so we can redirect again after login + const redirectAfterLogin = props.location.pathname; + props.dispatch(pushState(null, `/signin?next=${redirectAfterLogin}`)); } } + componentWillMount() { + this.checkRedirect(this.props); + } + + componentWillReceiveProps(nextProps) { + this.checkRedirect(nextProps); + } + render() { - if (!this.props.isAuthenticated) { - return (
-

No anonymous access!

-
) + + const { isAuthenticated = false } = this.props; + + if (isAuthenticated) { + // render the component that requires auth (passed to this wrapper) + return ( + + ) } - // render the component that requires auth (passed to this wrapper) - return ( - - ) + + return (
+

No anonymous access!

+
); + } } diff --git a/js-frontend/src/components/HeaderLinks.js b/js-frontend/src/components/HeaderLinks.js index dabf0be..3f106b0 100644 --- a/js-frontend/src/components/HeaderLinks.js +++ b/js-frontend/src/components/HeaderLinks.js @@ -17,8 +17,7 @@ import { signOut } from '../actions/signOut'; export class HeaderLinks extends React.Component { signOut(evt, key) { - debugger; - signOut(); + this.props.dispatch(signOut()); } render() { diff --git a/js-frontend/src/controls/bootstrap/EmailSignUpForm.js b/js-frontend/src/controls/bootstrap/EmailSignUpForm.js index 23aaa34..f5a4ebc 100644 --- a/js-frontend/src/controls/bootstrap/EmailSignUpForm.js +++ b/js-frontend/src/controls/bootstrap/EmailSignUpForm.js @@ -19,7 +19,7 @@ import {emailSignUpFormUpdate, emailSignUp} from '../../actions/signUp'; class EmailSignUpForm extends React.Component { static propTypes = { - endpoint: PropTypes.string, + //endpoint: PropTypes.string, inputProps: PropTypes.shape({ email: PropTypes.object, password: PropTypes.object, @@ -58,6 +58,7 @@ class EmailSignUpForm extends React.Component { } render () { + try { const disabled = ( @@ -120,10 +121,10 @@ class EmailSignUpForm extends React.Component { placeholder="Phone" className="email-sign-up-email" disabled={disabled} - value={read(this.props.auth, 'signUp.form.phone', '')} - errors={read(this.props.auth, 'signUp.errors.phone', {})} - onChange={this.handleInput.bind(this, "phone")} - {...this.props.inputProps.phone} /> + value={read(this.props.auth, 'signUp.form.phoneNumber', '')} + errors={read(this.props.auth, 'signUp.errors.phoneNumber', {})} + onChange={this.handleInput.bind(this, "phoneNumber")} + {...this.props.inputProps.phoneNumber} /> ({ @@ -19,7 +19,7 @@ export const customerInfoMap = ({ }, email, ssn, - "phoneNumber": phone, + "phoneNumber": phoneNumber, "address": { "street1": address1, "street2": address2, diff --git a/js-frontend/src/reducers/auth/user.js b/js-frontend/src/reducers/auth/user.js index 9b35470..4406a93 100644 --- a/js-frontend/src/reducers/auth/user.js +++ b/js-frontend/src/reducers/auth/user.js @@ -10,14 +10,7 @@ const userInitalState = { export const userReducer = (state = {...userInitalState}, action) => { switch(action.type) { - case T.AUTH.AUTHENTICATE_COMPLETE: { - const { user } = action; - return {...state, - attributes: user, - isSignedIn: !!user - }; - } - + case T.AUTH.AUTHENTICATE_COMPLETE: case T.AUTH.SIGN_IN_COMPLETE: { const { user } = action; return {...state, diff --git a/js-frontend/src/utils/api.js b/js-frontend/src/utils/api.js index c4e7796..e367ae8 100644 --- a/js-frontend/src/utils/api.js +++ b/js-frontend/src/utils/api.js @@ -4,7 +4,9 @@ import fetch from './fetch'; import { getEmailSignInUrl, - getEmailSignUpUrl + getEmailSignUpUrl, + getCurrentUserUrl, + getAccountsUrl } from "./sessionStorage"; import root from './root'; @@ -31,4 +33,52 @@ export function apiSignUp(body) { method: "post", body: root.JSON.stringify(body) }).then(parseResponse); +} + +export function apiGetCurrentUser() { + return fetch(getCurrentUserUrl(), { + headers: { + "Accept": "application/json", + "Content-Type": "application/json" + }, + method: "get" + }).then(parseResponse); +} + +export function apiCreateAccount(customerId, title, initialBalance) { + //{ + //"accountId": "0000015377cf131b-a250093f26850000" +//} + + return fetch(getAccountsUrl(), { + headers: { + "Accept": "application/json", + "Content-Type": "application/json" + }, + method: "post", + body: root.JSON.stringify({ customerId, title, initialBalance }) + }).then(parseResponse); +} + +export function apiRetrieveAccount(accountId) { + return fetch(`${getCurrentUserUrl()}/${accountId}`, { + headers: { + "Accept": "application/json", + "Content-Type": "application/json" + }, + method: "get" + }).then(parseResponse); +} + +export function apiRetrieveUsers(search) { + return fetch(getCurrentUserUrl(), { + headers: { + "Accept": "application/json", + "Content-Type": "application/json" + }, + method: "get", + body: { + email: search + } + }).then(parseResponse); } \ No newline at end of file diff --git a/js-frontend/src/utils/clientSettings.js b/js-frontend/src/utils/clientSettings.js index cb7ae6d..739bfbe 100644 --- a/js-frontend/src/utils/clientSettings.js +++ b/js-frontend/src/utils/clientSettings.js @@ -16,7 +16,8 @@ import { setCurrentEndpoint, setCurrentEndpointKey, retrieveData, - persistData + persistData, + destroySession } from "./sessionStorage"; // can't use "window" with node app @@ -68,6 +69,11 @@ const defaultSettings = { // save session configuration export function applyConfig({ dispatch, endpoint={}, settings={}, reset=false } = {}) { + + if (settings.currentLocation && settings.currentLocation.match(/blank=true/)) { + return Promise.resolve({blank: true}); + } + let currentEndpointKey; if (reset) { @@ -80,6 +86,16 @@ export function applyConfig({ dispatch, endpoint={}, settings={}, reset=false } setCurrentSettings({ ...defaultSettings, ...settings }); + const currentHeaders = retrieveData(C.SAVED_CREDS_KEY) || {}; + + const accessToken = currentHeaders["access-token"]; + + //if (authRedirectHeaders && authRedirectHeaders["access-token"]) { + if (!accessToken) { + destroySession(); + } + + let { defaultEndpointKey, currentEndpoint } = parseEndpointConfig( endpoint, getInitialEndpointKey() ); @@ -92,26 +108,15 @@ export function applyConfig({ dispatch, endpoint={}, settings={}, reset=false } setDefaultEndpointKey(defaultEndpointKey); setCurrentEndpoint(currentEndpoint); - dispatch(setEndpointKeys(Object.keys(currentEndpoint), currentEndpointKey, defaultEndpointKey)); + dispatch(setEndpointKeys( + Object.keys(currentEndpoint), + currentEndpointKey, + defaultEndpointKey)); + setCurrentEndpointKey(currentEndpointKey); - if (getCurrentSettings().initialCredentials) { - // skip initial headers check (i.e. check was already done server-side) - let { user, headers, config } = getCurrentSettings().initialCredentials; - persistData(C.SAVED_CREDS_KEY, headers); - return Promise.resolve(user); - } - - const savedCreds = retrieveData(C.SAVED_CREDS_KEY); - - if (savedCreds) { - // verify session credentials with API - debugger; - return fetch(savedCreds) - } - - return Promise.reject({ reason: "No credentials." }) + return Promise.resolve(); } \ No newline at end of file diff --git a/js-frontend/src/utils/constants.js b/js-frontend/src/utils/constants.js index 472d729..f1ad98c 100644 --- a/js-frontend/src/utils/constants.js +++ b/js-frontend/src/utils/constants.js @@ -4,4 +4,5 @@ export const INITIAL_CONFIG_KEY = "default"; export const DEFAULT_CONFIG_KEY = "defaultConfigKey"; export const SAVED_CONFIG_KEY = "currentConfigName"; -export const SAVED_CREDS_KEY = "authHeaders"; \ No newline at end of file +export const SAVED_CREDS_KEY = "authHeaders"; +export const SAVED_USER_INFO = "user-info"; \ No newline at end of file diff --git a/js-frontend/src/utils/handleFetchResponse.js b/js-frontend/src/utils/handleFetchResponse.js index 7e2f2ed..e93a13c 100644 --- a/js-frontend/src/utils/handleFetchResponse.js +++ b/js-frontend/src/utils/handleFetchResponse.js @@ -6,6 +6,24 @@ export function parseResponse (response) { if (response.status >= 200 && response.status < 300) { return json; } else { - return json.then(err => Promise.reject(err)); + + //error: "Bad Request" + //exception: "org.springframework.web.bind.MethodArgumentNotValidException" + //message: "Validation failed for argument at index 0 in method: public rx.Observable net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.customers.CustomerController.createCustomer(net.chrisrichardson.eventstore.javaexamples.banking.common.customers.CustomerInfo), with 3 error(s): [Field error in object 'customerInfo' on field 'ssn': rejected value [null]; codes [NotNull.customerInfo.ssn,NotNull.ssn,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [customerInfo.ssn,ssn]; arguments []; default message [ssn]]; default message [may not be null]] [Field error in object 'customerInfo' on field 'email': rejected value [null]; codes [NotNull.customerInfo.email,NotNull.email,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [customerInfo.email,email]; arguments []; default message [email]]; default message [may not be null]] [Field error in object 'customerInfo' on field 'phoneNumber': rejected value [null]; codes [NotNull.customerInfo.phoneNumber,NotNull.phoneNumber,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [customerInfo.phoneNumber,phoneNumber]; arguments []; default message [phoneNumber]]; default message [may not be null]] " + //path: "/customers" + //status: 400 + //timestamp: 1458002123103 + + return json.then(({ message, ...rest }) => { + if (!message) { + return rest; + } + const jvmPattern = /\[Field error in object 'customerInfo' on field '(\w+)'/gm; + let errors = {}; + message.replace(jvmPattern, (m, name) => { + errors[name] = ['Required']; + }); + return { errors }; + }).then(err => Promise.reject(err)); } } \ No newline at end of file diff --git a/js-frontend/src/utils/sessionStorage.js b/js-frontend/src/utils/sessionStorage.js index eb0b7b2..2690af8 100644 --- a/js-frontend/src/utils/sessionStorage.js +++ b/js-frontend/src/utils/sessionStorage.js @@ -12,7 +12,7 @@ import root from './root'; root.authState = { currentSettings: {}, currentEndpoint: {}, - defaultEndpointKey: null + defaultEndpointKey: 'default' }; export function setCurrentSettings (s) { @@ -67,7 +67,8 @@ export function resetConfig () { export function destroySession () { var sessionKeys = [ C.SAVED_CREDS_KEY, - C.SAVED_CONFIG_KEY + C.SAVED_CONFIG_KEY, + C.SAVED_USER_INFO ]; for (var key in sessionKeys) { @@ -122,6 +123,14 @@ export function getEmailSignUpUrl (endpointKey) { return `${getSessionEndpoint(endpointKey).emailRegistrationPath}` } +export function getCurrentUserUrl (endpointKey) { + return `${getSessionEndpoint(endpointKey).currentUserPath}` +} + +export function getAccountsUrl (endpointKey) { + return `${getSessionEndpoint(endpointKey).accountsPath}` +} + /** * @deprecated * @param key @@ -136,6 +145,17 @@ export function getTokenFormat() { return root.authState.currentSettings.tokenFormat; } +export function persistUserData(user) { + persistData(C.SAVED_USER_INFO, user); +} + +export function retrieveUserData() { + return retrieveData(C.SAVED_USER_INFO); +} + +export function retrieveHeaders() { + return retrieveData(C.SAVED_CREDS_KEY) || {}; +} export function persistData (key, val) { val = root.JSON.stringify(val); diff --git a/js-frontend/src/views/MyAccounts.js b/js-frontend/src/views/MyAccounts.js index 86c217e..61fe5fb 100644 --- a/js-frontend/src/views/MyAccounts.js +++ b/js-frontend/src/views/MyAccounts.js @@ -76,18 +76,30 @@ class MyAccounts extends React.Component { //const deployTooltip = ( // Create a new instance of this demo on your own Heroku server. //); - debugger; const user = this.props.auth.user.attributes; const { - email, - ssn, - name + email = '', + ssn = '', + name = {}, + phoneNumber = '', + address, + toAccounts } = user; + const { - firstName, - lastName + firstName = '', + lastName = '' } = name; + const { + city, + state, + street1, + street2, + zipCode + } = address; + + const { showAccountModal, show3rdPartyAccountModal, showDeleteAccountModal } = this.state; const { accountToRemove = null } = this.state; @@ -117,6 +129,11 @@ class MyAccounts extends React.Component { { email } + + Phone: + { phoneNumber } + + SSN: { ssn } diff --git a/js-frontend/src/views/SignIn.js b/js-frontend/src/views/SignIn.js index e3a5f89..6eac2e5 100644 --- a/js-frontend/src/views/SignIn.js +++ b/js-frontend/src/views/SignIn.js @@ -22,20 +22,22 @@ import EmailSignInForm from "../controls/bootstrap/EmailSignInForm"; export class SignIn extends React.Component { - componentWillMount() { - - - } - - componentWillReceiveProps(nextProps) { - if (nextProps.auth.user.isSignedIn) { -//debugger; - this.props.dispatch(pushState(null, nextProps.location.query.next)); + checkRedirect(props) { + if (props.auth.user.isSignedIn) { + props.dispatch(pushState(null, props.location.query.next)); //// redirect to login and add next param so we can redirect again after login //const redirectAfterLogin = this.props.location.pathname; //this.props.dispatch(pushState(null, `/signin?next=${redirectAfterLogin}`)); } } + componentWillMount() { + this.checkRedirect(this.props); + } + + componentWillReceiveProps(nextProps) { + this.checkRedirect(nextProps); + + } render () { const signInProps = { diff --git a/js-frontend/src/views/SignUp.js b/js-frontend/src/views/SignUp.js index f786565..76b421e 100644 --- a/js-frontend/src/views/SignUp.js +++ b/js-frontend/src/views/SignUp.js @@ -20,7 +20,7 @@ export class SignUp extends React.Component { Register - + );