mirror of
				https://github.com/elyby/accounts-frontend.git
				synced 2025-05-31 14:11:58 +05:30 
			
		
		
		
	Auth flow. The next
This commit is contained in:
		| @@ -13,7 +13,6 @@ import messages from './Activation.messages'; | ||||
| class Body extends BaseAuthBody { | ||||
|     static propTypes = { | ||||
|         ...BaseAuthBody.propTypes, | ||||
|         activate: PropTypes.func.isRequired, | ||||
|         auth: PropTypes.shape({ | ||||
|             error: PropTypes.string, | ||||
|             login: PropTypes.shape({ | ||||
| @@ -48,10 +47,6 @@ class Body extends BaseAuthBody { | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     onFormSubmit() { | ||||
|         this.props.activate(this.serialize()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default function Activation() { | ||||
|   | ||||
| @@ -22,7 +22,7 @@ export default class AppInfo extends Component { | ||||
|         return ( | ||||
|             <div className={styles.appInfo}> | ||||
|                 <div className={styles.logoContainer}> | ||||
|                     <h2 className={styles.logo}>{name}</h2> | ||||
|                     <h2 className={styles.logo}>{name || 'Ely Accounts'}</h2> | ||||
|                 </div> | ||||
|                 <div className={styles.descriptionContainer}> | ||||
|                     <p className={styles.description}> | ||||
|   | ||||
| @@ -8,6 +8,8 @@ import AuthError from './AuthError'; | ||||
| export default class BaseAuthBody extends Component { | ||||
|     static propTypes = { | ||||
|         clearErrors: PropTypes.func.isRequired, | ||||
|         resolve: PropTypes.func.isRequired, | ||||
|         reject: PropTypes.func.isRequired, | ||||
|         auth: PropTypes.shape({ | ||||
|             error: PropTypes.string | ||||
|         }) | ||||
| @@ -20,6 +22,10 @@ export default class BaseAuthBody extends Component { | ||||
|             ; | ||||
|     } | ||||
|  | ||||
|     onFormSubmit() { | ||||
|         this.props.resolve(this.serialize()); | ||||
|     } | ||||
|  | ||||
|     onClearErrors = this.props.clearErrors; | ||||
|  | ||||
|     form = {}; | ||||
|   | ||||
| @@ -14,7 +14,6 @@ import passwordMessages from './Password.messages'; | ||||
| class Body extends BaseAuthBody { | ||||
|     static propTypes = { | ||||
|         ...BaseAuthBody.propTypes, | ||||
|         login: PropTypes.func.isRequired, | ||||
|         auth: PropTypes.shape({ | ||||
|             error: PropTypes.string, | ||||
|             login: PropTypes.shape({ | ||||
| @@ -37,10 +36,6 @@ class Body extends BaseAuthBody { | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     onFormSubmit() { | ||||
|         this.props.login(this.serialize()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default function Login() { | ||||
|   | ||||
| @@ -1,25 +1,9 @@ | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import React, { Component } from 'react'; | ||||
|  | ||||
| import { connect } from 'react-redux'; | ||||
|  | ||||
| import { logout } from 'components/auth/actions'; | ||||
|  | ||||
| class Logout extends Component { | ||||
| export class Logout extends Component { | ||||
|     static displayName = 'Logout'; | ||||
|  | ||||
|     static propTypes = { | ||||
|         logout: PropTypes.func.isRequired | ||||
|     }; | ||||
|  | ||||
|     componentWillMount() { | ||||
|         this.props.logout(); | ||||
|     } | ||||
|  | ||||
|     render() { | ||||
|         return <span />; | ||||
|         return <span/>; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default connect(null, { | ||||
|     logout | ||||
| })(Logout); | ||||
|   | ||||
| @@ -1,43 +1,9 @@ | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
| import React, { Component } from 'react'; | ||||
|  | ||||
| import { connect } from 'react-redux'; | ||||
|  | ||||
| import { oAuthValidate, oAuthComplete } from 'components/auth/actions'; | ||||
|  | ||||
| class OAuthInit extends Component { | ||||
| export default class OAuthInit extends Component { | ||||
|     static displayName = 'OAuthInit'; | ||||
|  | ||||
|     static propTypes = { | ||||
|         query: PropTypes.shape({ | ||||
|             client_id: PropTypes.string.isRequired, | ||||
|             redirect_uri: PropTypes.string.isRequired, | ||||
|             response_type: PropTypes.string.isRequired, | ||||
|             scope: PropTypes.string.isRequired, | ||||
|             state: PropTypes.string | ||||
|         }), | ||||
|         validate: PropTypes.func.isRequired | ||||
|     }; | ||||
|  | ||||
|     componentWillMount() { | ||||
|         const {query} = this.props; | ||||
|  | ||||
|         this.props.validate({ | ||||
|             clientId: query.client_id, | ||||
|             redirectUrl: query.redirect_uri, | ||||
|             responseType: query.response_type, | ||||
|             scope: query.scope, | ||||
|             state: query.state | ||||
|         }).then(this.props.complete); | ||||
|     } | ||||
|  | ||||
|     render() { | ||||
|         return <span />; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default connect((state) => ({ | ||||
|     query: state.routing.location.query | ||||
| }), { | ||||
|     validate: oAuthValidate, | ||||
|     complete: oAuthComplete | ||||
| })(OAuthInit); | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import React, { Component, PropTypes } from 'react'; | ||||
|  | ||||
| import { connect } from 'react-redux'; | ||||
| import { routeActions } from 'react-router-redux'; | ||||
| import { TransitionMotion, spring } from 'react-motion'; | ||||
| import ReactHeight from 'react-height'; | ||||
|  | ||||
| @@ -10,6 +9,7 @@ import { Form } from 'components/ui/Form'; | ||||
| import {helpLinks as helpLinksStyles} from 'components/auth/helpLinks.scss'; | ||||
| import panelStyles from 'components/ui/panel.scss'; | ||||
| import icons from 'components/ui/icons.scss'; | ||||
| import authFlow from 'services/authFlow'; | ||||
|  | ||||
| import * as actions from './actions'; | ||||
|  | ||||
| @@ -28,7 +28,6 @@ class PanelTransition extends Component { | ||||
|                 password: PropTypes.string | ||||
|             }) | ||||
|         }).isRequired, | ||||
|         goBack: React.PropTypes.func.isRequired, | ||||
|         setError: React.PropTypes.func.isRequired, | ||||
|         clearErrors: React.PropTypes.func.isRequired, | ||||
|         path: PropTypes.string.isRequired, | ||||
| @@ -211,8 +210,7 @@ class PanelTransition extends Component { | ||||
|     onGoBack = (event) => { | ||||
|         event.preventDefault(); | ||||
|  | ||||
|         this.body.onGoBack && this.body.onGoBack(); | ||||
|         this.props.goBack(); | ||||
|         authFlow.goBack(); | ||||
|     }; | ||||
|  | ||||
|     getHeader(key, props) { | ||||
| @@ -341,14 +339,10 @@ class PanelTransition extends Component { | ||||
| export default connect((state) => ({ | ||||
|     user: state.user, | ||||
|     auth: state.auth, | ||||
|     path: state.routing.location.pathname | ||||
|     path: state.routing.location.pathname, | ||||
|     resolve: authFlow.resolve.bind(authFlow), | ||||
|     reject: authFlow.reject.bind(authFlow) | ||||
| }), { | ||||
|     goBack: routeActions.goBack, | ||||
|     login: actions.login, | ||||
|     logout: actions.logout, | ||||
|     register: actions.register, | ||||
|     activate: actions.activate, | ||||
|     clearErrors: actions.clearErrors, | ||||
|     oAuthComplete: actions.oAuthComplete, | ||||
|     setError: actions.setError | ||||
| })(PanelTransition); | ||||
|   | ||||
| @@ -15,8 +15,6 @@ import messages from './Password.messages'; | ||||
| class Body extends BaseAuthBody { | ||||
|     static propTypes = { | ||||
|         ...BaseAuthBody.propTypes, | ||||
|         login: PropTypes.func.isRequired, | ||||
|         logout: PropTypes.func.isRequired, | ||||
|         auth: PropTypes.shape({ | ||||
|             error: PropTypes.string, | ||||
|             login: PropTypes.shape({ | ||||
| @@ -56,17 +54,6 @@ class Body extends BaseAuthBody { | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     onFormSubmit() { | ||||
|         this.props.login({ | ||||
|             ...this.serialize(), | ||||
|             login: this.props.user.email || this.props.user.username | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     onGoBack() { | ||||
|         this.props.logout(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default function Password() { | ||||
|   | ||||
| @@ -15,15 +15,7 @@ import styles from './passwordChange.scss'; | ||||
|  | ||||
| class Body extends BaseAuthBody { | ||||
|     static propTypes = { | ||||
|         ...BaseAuthBody.propTypes/*, | ||||
|         // Я так полагаю, это правила валидации? | ||||
|         login: PropTypes.func.isRequired, | ||||
|         auth: PropTypes.shape({ | ||||
|             error: PropTypes.string, | ||||
|             login: PropTypes.shape({ | ||||
|                 login: PropTypes.stirng | ||||
|             }) | ||||
|         })*/ | ||||
|         ...BaseAuthBody.propTypes | ||||
|     }; | ||||
|  | ||||
|     render() { | ||||
| @@ -56,10 +48,6 @@ class Body extends BaseAuthBody { | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     onFormSubmit() { | ||||
|         this.props.login(this.serialize()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default function PasswordChange() { | ||||
| @@ -75,10 +63,14 @@ export default function PasswordChange() { | ||||
|                 <Message {...passwordChangedMessages.change} /> | ||||
|             </button> | ||||
|         ), | ||||
|         Links: () => ( | ||||
|             <Link to="/oauth/permissions"> | ||||
|         Links: (props) => ( | ||||
|             <a href="#" onClick={(event) => { | ||||
|                 event.preventDefault(); | ||||
|  | ||||
|                 props.reject(); | ||||
|             }}> | ||||
|                 <Message {...passwordChangedMessages.skipThisStep} /> | ||||
|             </Link> | ||||
|             </a> | ||||
|         ) | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -14,8 +14,6 @@ import messages from './Permissions.messages'; | ||||
| class Body extends BaseAuthBody { | ||||
|     static propTypes = { | ||||
|         ...BaseAuthBody.propTypes, | ||||
|         login: PropTypes.func.isRequired, | ||||
|         oAuthComplete: PropTypes.func.isRequired, | ||||
|         auth: PropTypes.shape({ | ||||
|             error: PropTypes.string, | ||||
|             scopes: PropTypes.array.isRequired | ||||
| @@ -52,20 +50,14 @@ class Body extends BaseAuthBody { | ||||
|                         <Message {...messages.theAppNeedsAccess2} /> | ||||
|                     </div> | ||||
|                     <ul className={styles.permissionsList}> | ||||
|                         {scopes.map((scope) => ( | ||||
|                             <li>{<Message {...messages[`scope_${scope}`]} />}</li> | ||||
|                         {scopes.map((scope, key) => ( | ||||
|                             <li key={key}>{<Message {...messages[`scope_${scope}`]} />}</li> | ||||
|                         ))} | ||||
|                     </ul> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     onFormSubmit() { | ||||
|         this.props.oAuthComplete({ | ||||
|             accept: true | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default function Permissions() { | ||||
| @@ -85,9 +77,7 @@ export default function Permissions() { | ||||
|             <a href="#" onClick={(event) => { | ||||
|                 event.preventDefault(); | ||||
|  | ||||
|                 props.onAuthComplete({ | ||||
|                     accept: false | ||||
|                 }); | ||||
|                 props.reject(); | ||||
|             }}> | ||||
|                 <Message {...messages.decline} /> | ||||
|             </a> | ||||
|   | ||||
| @@ -82,10 +82,6 @@ class Body extends BaseAuthBody { | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     onFormSubmit() { | ||||
|         this.props.register(this.serialize()); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default function Register() { | ||||
|   | ||||
| @@ -19,30 +19,26 @@ export function login({login = '', password = '', rememberMe = false}) { | ||||
|                 token: resp.jwt | ||||
|             })); | ||||
|  | ||||
|             dispatch(authenticate(resp.jwt)); | ||||
|  | ||||
|             dispatch(redirectToGoal()); | ||||
|             return dispatch(authenticate(resp.jwt)); | ||||
|         }) | ||||
|         .catch((resp) => { | ||||
|             if (resp.errors.login === ACTIVATION_REQUIRED) { | ||||
|                 dispatch(updateUser({ | ||||
|                 return dispatch(updateUser({ | ||||
|                     isActive: false, | ||||
|                     isGuest: false | ||||
|                 })); | ||||
|  | ||||
|                 dispatch(redirectToGoal()); | ||||
|             } else if (resp.errors.password === PASSWORD_REQUIRED) { | ||||
|                 dispatch(updateUser({ | ||||
|                 return dispatch(updateUser({ | ||||
|                     username: login, | ||||
|                     email: login | ||||
|                 })); | ||||
|                 dispatch(routeActions.push('/password')); | ||||
|             } else { | ||||
|                 if (resp.errors.login === LOGIN_REQUIRED && password) { | ||||
|                     dispatch(logout()); | ||||
|                 } | ||||
|                 const errorMessage = resp.errors[Object.keys(resp.errors)[0]]; | ||||
|                 dispatch(setError(errorMessage)); | ||||
|                 throw new Error(errorMessage); | ||||
|             } | ||||
|  | ||||
|             // TODO: log unexpected errors | ||||
| @@ -73,6 +69,7 @@ export function register({ | ||||
|         .catch((resp) => { | ||||
|             const errorMessage = resp.errors[Object.keys(resp.errors)[0]]; | ||||
|             dispatch(setError(errorMessage)); | ||||
|             throw new Error(errorMessage); | ||||
|  | ||||
|             // TODO: log unexpected errors | ||||
|         }) | ||||
| @@ -87,43 +84,22 @@ export function activate({key = ''}) { | ||||
|         ) | ||||
|         .then((resp) => { | ||||
|             dispatch(updateUser({ | ||||
|                 isGuest: false, | ||||
|                 isActive: true | ||||
|             })); | ||||
|  | ||||
|             dispatch(authenticate(resp.jwt)); | ||||
|  | ||||
|             dispatch(redirectToGoal()); | ||||
|             return dispatch(authenticate(resp.jwt)); | ||||
|         }) | ||||
|         .catch((resp) => { | ||||
|             const errorMessage = resp.errors[Object.keys(resp.errors)[0]]; | ||||
|             dispatch(setError(errorMessage)); | ||||
|             throw new Error(errorMessage); | ||||
|  | ||||
|             // TODO: log unexpected errors | ||||
|         }) | ||||
|         ; | ||||
| } | ||||
|  | ||||
| function redirectToGoal() { | ||||
|     return (dispatch, getState) => { | ||||
|         const {user} = getState(); | ||||
|  | ||||
|         switch (user.goal) { | ||||
|             case 'oauth': | ||||
|                 dispatch(routeActions.push('/oauth/permissions')); | ||||
|                 break; | ||||
|  | ||||
|             case 'account': | ||||
|             default: | ||||
|                 dispatch(routeActions.push('/')); | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|         // dispatch(updateUser({ // TODO: mb create action resetGoal? | ||||
|         //     goal: null | ||||
|         // })); | ||||
|     }; | ||||
| } | ||||
|  | ||||
| export const ERROR = 'error'; | ||||
| export function setError(error) { | ||||
|     return { | ||||
| @@ -138,10 +114,7 @@ export function clearErrors() { | ||||
| } | ||||
|  | ||||
| export function logout() { | ||||
|     return (dispatch) => { | ||||
|         dispatch(logoutUser()); | ||||
|         dispatch(routeActions.push('/login')); | ||||
|     }; | ||||
|     return logoutUser(); | ||||
| } | ||||
|  | ||||
| // TODO: move to oAuth actions? | ||||
| @@ -174,28 +147,26 @@ export function oAuthComplete(params = {}) { | ||||
|             `/api/oauth/complete?${query}`, | ||||
|             typeof params.accept === 'undefined' ? {} : {accept: params.accept} | ||||
|         ) | ||||
|         .then((resp) => { | ||||
|             if (resp.status === 401 && resp.name === 'Unauthorized') { | ||||
|                 // TODO: temporary solution for oauth init by guest | ||||
|                 // TODO: request serivce should handle http status codes | ||||
|                 dispatch(routeActions.push('/oauth/permissions')); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (resp.redirectUri) { | ||||
|                 location.href = resp.redirectUri; | ||||
|             } | ||||
|         }) | ||||
|         .catch((resp = {}) => { // TODO | ||||
|             handleOauthParamsValidation(resp); | ||||
|  | ||||
|             if (resp.statusCode === 401 && resp.error === 'accept_required') { | ||||
|                 dispatch(routeActions.push('/oauth/permissions')); | ||||
|             } | ||||
|  | ||||
|             if (resp.statusCode === 401 && resp.error === 'access_denied') { | ||||
|                 // user declined permissions | ||||
|                 location.href = resp.redirectUri; | ||||
|                 return { | ||||
|                     redirectUri: resp.redirectUri | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             handleOauthParamsValidation(resp); | ||||
|  | ||||
|             if (resp.status === 401 && resp.name === 'Unauthorized') { | ||||
|                 const error = new Error('Unauthorized'); | ||||
|                 error.unauthorized = true; | ||||
|                 throw error; | ||||
|             } | ||||
|  | ||||
|             if (resp.statusCode === 401 && resp.error === 'accept_required') { | ||||
|                 const error = new Error('Permissions accept required'); | ||||
|                 error.acceptRequired = true; | ||||
|                 throw error; | ||||
|             } | ||||
|         }); | ||||
|     }; | ||||
| @@ -212,17 +183,22 @@ function getOAuthRequest(oauth) { | ||||
| } | ||||
|  | ||||
| function handleOauthParamsValidation(resp = {}) { | ||||
|     const error = new Error('Error completing request'); | ||||
|     if (resp.statusCode === 400 && resp.error === 'invalid_request') { | ||||
|         alert(`Invalid request (${resp.parameter} required).`); | ||||
|         throw error; | ||||
|     } | ||||
|     if (resp.statusCode === 400 && resp.error === 'unsupported_response_type') { | ||||
|         alert(`Invalid response type '${resp.parameter}'.`); | ||||
|         throw error; | ||||
|     } | ||||
|     if (resp.statusCode === 400 && resp.error === 'invalid_scope') { | ||||
|         alert(`Invalid scope '${resp.parameter}'.`); | ||||
|         throw error; | ||||
|     } | ||||
|     if (resp.statusCode === 401 && resp.error === 'invalid_client') { | ||||
|         alert('Can not find application you are trying to authorize.'); | ||||
|         throw error; | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -53,6 +53,6 @@ export function authenticate(token) { | ||||
|  | ||||
|     return (dispatch) => { | ||||
|         request.setAuthToken(token); | ||||
|         dispatch(fetchUserData()); | ||||
|         return dispatch(fetchUserData()); | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -1,16 +1,32 @@ | ||||
| import React, { Component } from 'react'; | ||||
|  | ||||
| import AuthPage from 'pages/auth/AuthPage'; | ||||
| import Login from 'components/auth/Login'; | ||||
| import { connect } from 'react-redux'; | ||||
|  | ||||
| export default class IndexPage extends Component { | ||||
| import authFlow from 'services/authFlow'; | ||||
|  | ||||
| class IndexPage extends Component { | ||||
|     displayName = 'IndexPage'; | ||||
|  | ||||
|     componentWillMount() { | ||||
|         if (this.props.user.isGuest) { | ||||
|             authFlow.login(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     render() { | ||||
|         const {user, children} = this.props; | ||||
|  | ||||
|         return ( | ||||
|             <AuthPage> | ||||
|                 <Login /> | ||||
|             </AuthPage> | ||||
|             <div> | ||||
|                 <h1> | ||||
|                     Hello {user.username}! | ||||
|                 </h1> | ||||
|                 {children} | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default connect((state) => ({ | ||||
|     user: state.user | ||||
| }))(IndexPage); | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import RootPage from 'pages/root/RootPage'; | ||||
| import IndexPage from 'pages/index/IndexPage'; | ||||
| import AuthPage from 'pages/auth/AuthPage'; | ||||
|  | ||||
| import { authenticate, updateUser } from 'components/user/actions'; | ||||
| import { authenticate } from 'components/user/actions'; | ||||
|  | ||||
| import OAuthInit from 'components/auth/OAuthInit'; | ||||
| import Register from 'components/auth/Register'; | ||||
| @@ -17,72 +17,37 @@ import Logout from 'components/auth/Logout'; | ||||
| import PasswordChange from 'components/auth/PasswordChange'; | ||||
| import ForgotPassword from 'components/auth/ForgotPassword'; | ||||
|  | ||||
| import authFlow from 'services/authFlow'; | ||||
|  | ||||
| export default function routesFactory(store) { | ||||
|     function checkAuth(nextState, replace) { | ||||
|         const state = store.getState(); | ||||
|         const pathname = state.routing.location.pathname; | ||||
|  | ||||
|         let forcePath; | ||||
|         let goal; | ||||
|         if (!state.user.isGuest) { | ||||
|             if (!state.user.isActive) { | ||||
|                 forcePath = '/activation'; | ||||
|             } else if (!state.user.shouldChangePassword) { | ||||
|                 forcePath = '/password-change'; | ||||
|             } | ||||
|         } else { | ||||
|             if (state.user.email || state.user.username) { | ||||
|                 forcePath = '/password'; | ||||
|             } else { | ||||
|                 forcePath = '/login'; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // TODO: validate that we have all required data on premissions page | ||||
|  | ||||
|         if (forcePath && pathname !== forcePath) { | ||||
|             switch (pathname) { | ||||
|                 case '/': | ||||
|                     goal = 'account'; | ||||
|                     break; | ||||
|  | ||||
|                 case '/oauth/permissions': | ||||
|                     goal = 'oauth'; | ||||
|                     break; | ||||
|             } | ||||
|  | ||||
|             if (goal) { | ||||
|                 store.dispatch(updateUser({ // TODO: mb create action resetGoal? | ||||
|                     goal | ||||
|                 })); | ||||
|             } | ||||
|  | ||||
|             replace({pathname: forcePath}); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     const state = store.getState(); | ||||
|     if (state.user.token) { | ||||
|         // authorizing user if it is possible | ||||
|         store.dispatch(authenticate(state.user.token)); | ||||
|     } | ||||
|  | ||||
|     authFlow.setStore(store); | ||||
|  | ||||
|     const onEnter = { | ||||
|         onEnter: ({location}, replace) => authFlow.handleRequest(location.pathname, replace) | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
|         <Route path="/" component={RootPage}> | ||||
|             <IndexRoute component={IndexPage} onEnter={checkAuth} /> | ||||
|             <IndexRoute component={IndexPage} /> | ||||
|  | ||||
|             <Route path="oauth" component={OAuthInit} {...onEnter} /> | ||||
|             <Route path="logout" component={Logout} {...onEnter} /> | ||||
|  | ||||
|             <Route path="auth" component={AuthPage}> | ||||
|                 <Route path="/login" components={new Login()} onEnter={checkAuth} /> | ||||
|                 <Route path="/password" components={new Password()} onEnter={checkAuth} /> | ||||
|                 <Route path="/register" components={new Register()} /> | ||||
|                 <Route path="/activation" components={new Activation()} /> | ||||
|                 <Route path="/oauth/permissions" components={new Permissions()} onEnter={checkAuth} /> | ||||
|                 <Route path="/password-change" components={new PasswordChange()} /> | ||||
|                 <Route path="/forgot-password" components={new ForgotPassword()} /> | ||||
|                 <Route path="/login" components={new Login()} {...onEnter} /> | ||||
|                 <Route path="/password" components={new Password()} {...onEnter} /> | ||||
|                 <Route path="/register" components={new Register()} {...onEnter} /> | ||||
|                 <Route path="/activation" components={new Activation()} {...onEnter} /> | ||||
|                 <Route path="/oauth/permissions" components={new Permissions()} {...onEnter} /> | ||||
|                 <Route path="/password-change" components={new PasswordChange()} {...onEnter} /> | ||||
|                 <Route path="/forgot-password" components={new ForgotPassword()} {...onEnter} /> | ||||
|             </Route> | ||||
|  | ||||
|             <Route path="oauth" component={OAuthInit} /> | ||||
|             <Route path="logout" component={Logout} /> | ||||
|         </Route> | ||||
|     ); | ||||
| } | ||||
|   | ||||
							
								
								
									
										5
									
								
								src/services/authFlow.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/services/authFlow.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| import AuthFlow from './authFlow/AuthFlow'; | ||||
|  | ||||
| // TODO: a way to unload service (when we are on account page) | ||||
|  | ||||
| export default new AuthFlow(); | ||||
							
								
								
									
										9
									
								
								src/services/authFlow/AbstractState.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/services/authFlow/AbstractState.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| export default class AbstractState { | ||||
|     resolve() {} | ||||
|     goBack() { | ||||
|         throw new Error('There is no way back'); | ||||
|     } | ||||
|     reject() {} | ||||
|     enter() {} | ||||
|     leave() {} | ||||
| } | ||||
							
								
								
									
										19
									
								
								src/services/authFlow/ActivationState.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/services/authFlow/ActivationState.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import AbstractState from './AbstractState'; | ||||
| import CompleteState from './CompleteState'; | ||||
|  | ||||
| export default class ActivationState extends AbstractState { | ||||
|     enter(context) { | ||||
|         const {user} = context.getState(); | ||||
|  | ||||
|         if (user.isActive) { | ||||
|             context.setState(new CompleteState()); | ||||
|         } else { | ||||
|             context.navigate('/activation'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     resolve(context, payload) { | ||||
|         context.run('activate', payload) | ||||
|             .then(() => context.setState(new CompleteState())); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										116
									
								
								src/services/authFlow/AuthFlow.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/services/authFlow/AuthFlow.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| import { routeActions } from 'react-router-redux'; | ||||
|  | ||||
| import * as actions from 'components/auth/actions'; | ||||
| import {updateUser} from 'components/user/actions'; | ||||
|  | ||||
| import RegisterState from './RegisterState'; | ||||
| import LoginState from './LoginState'; | ||||
| import OAuthState from './OAuthState'; | ||||
| import ForgotPasswordState from './ForgotPasswordState'; | ||||
|  | ||||
| const availableActions = { | ||||
|     ...actions, | ||||
|     updateUser | ||||
| }; | ||||
|  | ||||
| export default class AuthFlow { | ||||
|     constructor(states) { | ||||
|         this.states = states; | ||||
|     } | ||||
|  | ||||
|     setStore(store) { | ||||
|         this.navigate = (route) => { | ||||
|             const {routing} = this.getState(); | ||||
|  | ||||
|             if (routing.location.pathname !== route) { | ||||
|                 this.ignoreRequest = true; // TODO: remove me | ||||
|                 if (this.replace) { | ||||
|                     this.replace(route); | ||||
|                 } | ||||
|                 store.dispatch(routeActions.push(route)); | ||||
|             } | ||||
|  | ||||
|             this.replace = null; | ||||
|         }; | ||||
|  | ||||
|         this.getState = store.getState.bind(store); | ||||
|         this.dispatch = store.dispatch.bind(store); | ||||
|     } | ||||
|  | ||||
|     resolve(payload = {}) { | ||||
|         this.state.resolve(this, payload); | ||||
|     } | ||||
|  | ||||
|     reject(payload = {}) { | ||||
|         this.state.reject(this, payload); | ||||
|     } | ||||
|  | ||||
|     goBack() { | ||||
|         this.state.goBack(this); | ||||
|     } | ||||
|  | ||||
|     run(actionId, payload) { | ||||
|         if (!availableActions[actionId]) { | ||||
|             throw new Error(`Action ${actionId} does not exists`); | ||||
|         } | ||||
|  | ||||
|         return this.dispatch(availableActions[actionId](payload)); | ||||
|     } | ||||
|  | ||||
|     setState(state) { | ||||
|         if (!state) { | ||||
|             throw new Error('State is required'); | ||||
|         } | ||||
|  | ||||
|         if (this.state instanceof state.constructor) { | ||||
|             // already in this state | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.state && this.state.leave(this); | ||||
|         this.state = state; | ||||
|         this.state.enter(this); | ||||
|     } | ||||
|  | ||||
|     handleRequest(path, replace) { | ||||
|         this.replace = replace; | ||||
|         if (this.ignoreRequest) { | ||||
|             this.ignoreRequest = false; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         switch (path) { | ||||
|             case '/oauth': | ||||
|                 this.setState(new OAuthState()); | ||||
|                 break; | ||||
|  | ||||
|             case '/register': | ||||
|                 this.setState(new RegisterState()); | ||||
|                 break; | ||||
|  | ||||
|             case '/forgot-password': | ||||
|                 this.setState(new ForgotPasswordState()); | ||||
|                 break; | ||||
|  | ||||
|             case '/login': | ||||
|             case '/password': | ||||
|             case '/activation': | ||||
|             case '/password-change': | ||||
|             case '/oauth/permissions': | ||||
|                 this.setState(new LoginState()); | ||||
|                 break; | ||||
|  | ||||
|             case '/logout': | ||||
|                 this.run('logout'); | ||||
|                 this.setState(new LoginState()); | ||||
|                 break; | ||||
|  | ||||
|             default: | ||||
|                 throw new Error(`Unsupported request: ${path}`); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     login() { | ||||
|         this.setState(new LoginState()); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										15
									
								
								src/services/authFlow/ChangePasswordState.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/services/authFlow/ChangePasswordState.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import AbstractState from './AbstractState'; | ||||
| import CompleteState from './CompleteState'; | ||||
|  | ||||
| export default class ChangePasswordState extends AbstractState { | ||||
|     enter(context) { | ||||
|         context.navigate('/password-change'); | ||||
|     } | ||||
|  | ||||
|     reject(context) { | ||||
|         context.run('updateUser', { | ||||
|             shouldChangePassword: false | ||||
|         }); | ||||
|         context.setState(new CompleteState()); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										32
									
								
								src/services/authFlow/CompleteState.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/services/authFlow/CompleteState.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| import AbstractState from './AbstractState'; | ||||
| import LoginState from './LoginState'; | ||||
| import PermissionsState from './PermissionsState'; | ||||
| import ActivationState from './ActivationState'; | ||||
| import ChangePasswordState from './ChangePasswordState'; | ||||
|  | ||||
| export default class CompleteState extends AbstractState { | ||||
|     enter(context) { | ||||
|         const {auth, user} = context.getState(); | ||||
|  | ||||
|         if (user.isGuest) { | ||||
|             context.setState(new LoginState()); | ||||
|         } else if (!user.isActive) { | ||||
|             context.setState(new ActivationState()); | ||||
|         } else if (user.shouldChangePassword) { | ||||
|             context.setState(new ChangePasswordState()); | ||||
|         } else if (auth.oauth) { | ||||
|             context.run('oAuthComplete').then((resp) => { | ||||
|                 location.href = resp.redirectUri; | ||||
|             }, (resp) => { | ||||
|                 // TODO | ||||
|                 if (resp.unauthorized) { | ||||
|                     context.setState(new LoginState()); | ||||
|                 } else if (resp.acceptRequired) { | ||||
|                     context.setState(new PermissionsState()); | ||||
|                 } | ||||
|             }); | ||||
|         } else { | ||||
|             context.navigate('/'); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										16
									
								
								src/services/authFlow/ForgotPasswordState.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/services/authFlow/ForgotPasswordState.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import AbstractState from './AbstractState'; | ||||
| import LoginState from './LoginState'; | ||||
|  | ||||
| export default class ForgotPasswordState extends AbstractState { | ||||
|     enter(context) { | ||||
|         context.navigate('/forgot-password'); | ||||
|     } | ||||
|  | ||||
|     goBack(context) { | ||||
|         context.setState(new LoginState()); | ||||
|     } | ||||
|  | ||||
|     reject(context) { | ||||
|         context.navigate('/send-message'); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										24
									
								
								src/services/authFlow/LoginState.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/services/authFlow/LoginState.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| import AbstractState from './AbstractState'; | ||||
| import PasswordState from './PasswordState'; | ||||
| import ForgotPasswordState from './ForgotPasswordState'; | ||||
|  | ||||
| export default class LoginState extends AbstractState { | ||||
|     enter(context) { | ||||
|         const {user} = context.getState(); | ||||
|  | ||||
|         if (user.email || user.username) { | ||||
|             context.setState(new PasswordState()); | ||||
|         } else { | ||||
|             context.navigate('/login'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     resolve(context, payload) { | ||||
|         context.run('login', payload) | ||||
|             .then(() => context.setState(new PasswordState())); | ||||
|     } | ||||
|  | ||||
|     reject(context) { | ||||
|         context.setState(new ForgotPasswordState()); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										16
									
								
								src/services/authFlow/OAuthState.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/services/authFlow/OAuthState.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import AbstractState from './AbstractState'; | ||||
| import CompleteState from './CompleteState'; | ||||
|  | ||||
| export default class OAuthState extends AbstractState { | ||||
|     enter(context) { | ||||
|         const query = context.getState().routing.location.query; | ||||
|  | ||||
|         context.run('oAuthValidate', { | ||||
|             clientId: query.client_id, | ||||
|             redirectUrl: query.redirect_uri, | ||||
|             responseType: query.response_type, | ||||
|             scope: query.scope, | ||||
|             state: query.state | ||||
|         }).then(() => context.setState(new CompleteState())); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										34
									
								
								src/services/authFlow/PasswordState.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/services/authFlow/PasswordState.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import AbstractState from './AbstractState'; | ||||
| import CompleteState from './CompleteState'; | ||||
| import ForgotPasswordState from './ForgotPasswordState'; | ||||
|  | ||||
| export default class PasswordState extends AbstractState { | ||||
|     enter(context) { | ||||
|         const {user} = context.getState(); | ||||
|  | ||||
|         if (!user.isGuest) { | ||||
|             context.setState(new CompleteState()); | ||||
|         } else { | ||||
|             context.navigate('/password'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     resolve(context, {password}) { | ||||
|         const {user} = context.getState(); | ||||
|  | ||||
|         context.run('login', { | ||||
|             password, | ||||
|             login: user.email || user.username | ||||
|         }) | ||||
|         .then(() => context.setState(new CompleteState())); | ||||
|     } | ||||
|  | ||||
|     reject(context) { | ||||
|         context.setState(new ForgotPasswordState()); | ||||
|     } | ||||
|  | ||||
|     goBack(context) { | ||||
|         context.run('logout'); | ||||
|         context.setState(new LoginState()); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										21
									
								
								src/services/authFlow/PermissionsState.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/services/authFlow/PermissionsState.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import AbstractState from './AbstractState'; | ||||
|  | ||||
| export default class PermissionsState extends AbstractState { | ||||
|     enter(context) { | ||||
|         context.navigate('/oauth/permissions'); | ||||
|     } | ||||
|  | ||||
|     resolve(context) { | ||||
|         this.process(context, true); | ||||
|     } | ||||
|  | ||||
|     reject(context) { | ||||
|         this.process(context, false); | ||||
|     } | ||||
|  | ||||
|     process(context, accept) { | ||||
|         context.run('oAuthComplete', { | ||||
|             accept | ||||
|         }).then((resp) => location.href = resp.redirectUri); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										22
									
								
								src/services/authFlow/RegisterState.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/services/authFlow/RegisterState.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import AbstractState from './AbstractState'; | ||||
| import CompleteState from './CompleteState'; | ||||
|  | ||||
| export default class RegisterState extends AbstractState { | ||||
|     enter(context) { | ||||
|         const {user} = context.getState(); | ||||
|  | ||||
|         if (!user.isGuest) { | ||||
|             context.setState(new CompleteState()); | ||||
|         } else { | ||||
|             context.navigate('/register'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     resolve(context, payload) { | ||||
|         context.run('register', payload) | ||||
|             .then(() => context.setState(new CompleteState())); | ||||
|     } | ||||
|  | ||||
|     reject(context) { | ||||
|     } | ||||
| } | ||||
| @@ -28,9 +28,11 @@ function buildQuery(data) { | ||||
|  | ||||
| let authToken; | ||||
|  | ||||
| const checkStatus = (resp) => Promise[resp.status >= 200 && resp.status < 300 ? 'resolve' : 'reject'](resp); | ||||
| const toJSON = (resp) => resp.json(); | ||||
| // if resp.success does not exist - degradating to HTTP status codes | ||||
| const rejectWithJSON = (resp) => toJSON(resp).then((resp) => {throw resp;}); | ||||
| const handleResponse = (resp) => Promise[resp.success || typeof resp.success === 'undefined' ? 'resolve' : 'reject'](resp); | ||||
|  | ||||
| const getDefaultHeaders = () => { | ||||
|     const header = {Accept: 'application/json'}; | ||||
|  | ||||
| @@ -51,7 +53,8 @@ export default { | ||||
|             }, | ||||
|             body: buildQuery(data) | ||||
|         }) | ||||
|         .then(toJSON) | ||||
|         .then(checkStatus) | ||||
|         .then(toJSON, rejectWithJSON) | ||||
|         .then(handleResponse) | ||||
|         ; | ||||
|     }, | ||||
| @@ -65,7 +68,8 @@ export default { | ||||
|         return fetch(url, { | ||||
|             headers: getDefaultHeaders() | ||||
|         }) | ||||
|         .then(toJSON) | ||||
|         .then(checkStatus) | ||||
|         .then(toJSON, rejectWithJSON) | ||||
|         .then(handleResponse) | ||||
|         ; | ||||
|     }, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user