From 57f0cf30e6bab8174c76ae95da90be687cf3c8d7 Mon Sep 17 00:00:00 2001 From: SleepWalker Date: Tue, 1 Mar 2016 22:36:14 +0200 Subject: [PATCH] Auth flow. The next --- src/components/auth/Activation.jsx | 5 - src/components/auth/AppInfo.jsx | 2 +- src/components/auth/BaseAuthBody.jsx | 6 + src/components/auth/Login.jsx | 5 - src/components/auth/Logout.jsx | 22 +--- src/components/auth/OAuthInit.jsx | 38 +----- src/components/auth/PanelTransition.jsx | 16 +-- src/components/auth/Password.jsx | 13 --- src/components/auth/PasswordChange.jsx | 24 ++-- src/components/auth/Permissions.jsx | 16 +-- src/components/auth/Register.jsx | 4 - src/components/auth/actions.js | 86 +++++--------- src/components/user/actions.js | 2 +- src/pages/index/IndexPage.jsx | 28 ++++- src/routes.js | 75 ++++-------- src/services/authFlow.js | 5 + src/services/authFlow/AbstractState.js | 9 ++ src/services/authFlow/ActivationState.js | 19 +++ src/services/authFlow/AuthFlow.js | 116 +++++++++++++++++++ src/services/authFlow/ChangePasswordState.js | 15 +++ src/services/authFlow/CompleteState.js | 32 +++++ src/services/authFlow/ForgotPasswordState.js | 16 +++ src/services/authFlow/LoginState.js | 24 ++++ src/services/authFlow/OAuthState.js | 16 +++ src/services/authFlow/PasswordState.js | 34 ++++++ src/services/authFlow/PermissionsState.js | 21 ++++ src/services/authFlow/RegisterState.js | 22 ++++ src/services/request.js | 10 +- 28 files changed, 438 insertions(+), 243 deletions(-) create mode 100644 src/services/authFlow.js create mode 100644 src/services/authFlow/AbstractState.js create mode 100644 src/services/authFlow/ActivationState.js create mode 100644 src/services/authFlow/AuthFlow.js create mode 100644 src/services/authFlow/ChangePasswordState.js create mode 100644 src/services/authFlow/CompleteState.js create mode 100644 src/services/authFlow/ForgotPasswordState.js create mode 100644 src/services/authFlow/LoginState.js create mode 100644 src/services/authFlow/OAuthState.js create mode 100644 src/services/authFlow/PasswordState.js create mode 100644 src/services/authFlow/PermissionsState.js create mode 100644 src/services/authFlow/RegisterState.js diff --git a/src/components/auth/Activation.jsx b/src/components/auth/Activation.jsx index 4804e9c..b02bf18 100644 --- a/src/components/auth/Activation.jsx +++ b/src/components/auth/Activation.jsx @@ -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 { ); } - - onFormSubmit() { - this.props.activate(this.serialize()); - } } export default function Activation() { diff --git a/src/components/auth/AppInfo.jsx b/src/components/auth/AppInfo.jsx index c987088..9a91306 100644 --- a/src/components/auth/AppInfo.jsx +++ b/src/components/auth/AppInfo.jsx @@ -22,7 +22,7 @@ export default class AppInfo extends Component { return (
-

{name}

+

{name || 'Ely Accounts'}

diff --git a/src/components/auth/BaseAuthBody.jsx b/src/components/auth/BaseAuthBody.jsx index 295f49a..f83412a 100644 --- a/src/components/auth/BaseAuthBody.jsx +++ b/src/components/auth/BaseAuthBody.jsx @@ -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 = {}; diff --git a/src/components/auth/Login.jsx b/src/components/auth/Login.jsx index 2f17d5c..0bcf7bc 100644 --- a/src/components/auth/Login.jsx +++ b/src/components/auth/Login.jsx @@ -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 {

); } - - onFormSubmit() { - this.props.login(this.serialize()); - } } export default function Login() { diff --git a/src/components/auth/Logout.jsx b/src/components/auth/Logout.jsx index 6d99ec8..7fcb2bf 100644 --- a/src/components/auth/Logout.jsx +++ b/src/components/auth/Logout.jsx @@ -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 ; + return ; } } - -export default connect(null, { - logout -})(Logout); diff --git a/src/components/auth/OAuthInit.jsx b/src/components/auth/OAuthInit.jsx index 858e327..c9ccd31 100644 --- a/src/components/auth/OAuthInit.jsx +++ b/src/components/auth/OAuthInit.jsx @@ -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 ; } } - -export default connect((state) => ({ - query: state.routing.location.query -}), { - validate: oAuthValidate, - complete: oAuthComplete -})(OAuthInit); diff --git a/src/components/auth/PanelTransition.jsx b/src/components/auth/PanelTransition.jsx index 229318f..ae3bc43 100644 --- a/src/components/auth/PanelTransition.jsx +++ b/src/components/auth/PanelTransition.jsx @@ -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); diff --git a/src/components/auth/Password.jsx b/src/components/auth/Password.jsx index 3585610..2c3913f 100644 --- a/src/components/auth/Password.jsx +++ b/src/components/auth/Password.jsx @@ -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 {
); } - - onFormSubmit() { - this.props.login({ - ...this.serialize(), - login: this.props.user.email || this.props.user.username - }); - } - - onGoBack() { - this.props.logout(); - } } export default function Password() { diff --git a/src/components/auth/PasswordChange.jsx b/src/components/auth/PasswordChange.jsx index cb8302c..a326232 100644 --- a/src/components/auth/PasswordChange.jsx +++ b/src/components/auth/PasswordChange.jsx @@ -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 { ); } - - onFormSubmit() { - this.props.login(this.serialize()); - } } export default function PasswordChange() { @@ -75,10 +63,14 @@ export default function PasswordChange() { ), - Links: () => ( - + Links: (props) => ( + { + event.preventDefault(); + + props.reject(); + }}> - + ) }; } diff --git a/src/components/auth/Permissions.jsx b/src/components/auth/Permissions.jsx index c057bfa..effe588 100644 --- a/src/components/auth/Permissions.jsx +++ b/src/components/auth/Permissions.jsx @@ -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 {
    - {scopes.map((scope) => ( -
  • {}
  • + {scopes.map((scope, key) => ( +
  • {}
  • ))}
); } - - onFormSubmit() { - this.props.oAuthComplete({ - accept: true - }); - } } export default function Permissions() { @@ -85,9 +77,7 @@ export default function Permissions() { { event.preventDefault(); - props.onAuthComplete({ - accept: false - }); + props.reject(); }}> diff --git a/src/components/auth/Register.jsx b/src/components/auth/Register.jsx index 9dd32b2..7afb324 100644 --- a/src/components/auth/Register.jsx +++ b/src/components/auth/Register.jsx @@ -82,10 +82,6 @@ class Body extends BaseAuthBody { ); } - - onFormSubmit() { - this.props.register(this.serialize()); - } } export default function Register() { diff --git a/src/components/auth/actions.js b/src/components/auth/actions.js index 1e9a031..87f6ed1 100644 --- a/src/components/auth/actions.js +++ b/src/components/auth/actions.js @@ -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; } } diff --git a/src/components/user/actions.js b/src/components/user/actions.js index 76027d4..adbd142 100644 --- a/src/components/user/actions.js +++ b/src/components/user/actions.js @@ -53,6 +53,6 @@ export function authenticate(token) { return (dispatch) => { request.setAuthToken(token); - dispatch(fetchUserData()); + return dispatch(fetchUserData()); }; } diff --git a/src/pages/index/IndexPage.jsx b/src/pages/index/IndexPage.jsx index 537326c..427985f 100644 --- a/src/pages/index/IndexPage.jsx +++ b/src/pages/index/IndexPage.jsx @@ -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 ( - - - +
+

+ Hello {user.username}! +

+ {children} +
); } } + +export default connect((state) => ({ + user: state.user +}))(IndexPage); diff --git a/src/routes.js b/src/routes.js index 7aeeecc..679c4e1 100644 --- a/src/routes.js +++ b/src/routes.js @@ -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 ( - + + + + - - - - - - - + + + + + + + - - - ); } diff --git a/src/services/authFlow.js b/src/services/authFlow.js new file mode 100644 index 0000000..1e4226a --- /dev/null +++ b/src/services/authFlow.js @@ -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(); diff --git a/src/services/authFlow/AbstractState.js b/src/services/authFlow/AbstractState.js new file mode 100644 index 0000000..386393f --- /dev/null +++ b/src/services/authFlow/AbstractState.js @@ -0,0 +1,9 @@ +export default class AbstractState { + resolve() {} + goBack() { + throw new Error('There is no way back'); + } + reject() {} + enter() {} + leave() {} +} diff --git a/src/services/authFlow/ActivationState.js b/src/services/authFlow/ActivationState.js new file mode 100644 index 0000000..ca35865 --- /dev/null +++ b/src/services/authFlow/ActivationState.js @@ -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())); + } +} diff --git a/src/services/authFlow/AuthFlow.js b/src/services/authFlow/AuthFlow.js new file mode 100644 index 0000000..c071050 --- /dev/null +++ b/src/services/authFlow/AuthFlow.js @@ -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()); + } +} diff --git a/src/services/authFlow/ChangePasswordState.js b/src/services/authFlow/ChangePasswordState.js new file mode 100644 index 0000000..531ae7c --- /dev/null +++ b/src/services/authFlow/ChangePasswordState.js @@ -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()); + } +} diff --git a/src/services/authFlow/CompleteState.js b/src/services/authFlow/CompleteState.js new file mode 100644 index 0000000..153b367 --- /dev/null +++ b/src/services/authFlow/CompleteState.js @@ -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('/'); + } + } +} diff --git a/src/services/authFlow/ForgotPasswordState.js b/src/services/authFlow/ForgotPasswordState.js new file mode 100644 index 0000000..89d6131 --- /dev/null +++ b/src/services/authFlow/ForgotPasswordState.js @@ -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'); + } +} diff --git a/src/services/authFlow/LoginState.js b/src/services/authFlow/LoginState.js new file mode 100644 index 0000000..293aea7 --- /dev/null +++ b/src/services/authFlow/LoginState.js @@ -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()); + } +} diff --git a/src/services/authFlow/OAuthState.js b/src/services/authFlow/OAuthState.js new file mode 100644 index 0000000..de788b6 --- /dev/null +++ b/src/services/authFlow/OAuthState.js @@ -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())); + } +} diff --git a/src/services/authFlow/PasswordState.js b/src/services/authFlow/PasswordState.js new file mode 100644 index 0000000..3ad1ff8 --- /dev/null +++ b/src/services/authFlow/PasswordState.js @@ -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()); + } +} diff --git a/src/services/authFlow/PermissionsState.js b/src/services/authFlow/PermissionsState.js new file mode 100644 index 0000000..87ab524 --- /dev/null +++ b/src/services/authFlow/PermissionsState.js @@ -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); + } +} diff --git a/src/services/authFlow/RegisterState.js b/src/services/authFlow/RegisterState.js new file mode 100644 index 0000000..cdc03c9 --- /dev/null +++ b/src/services/authFlow/RegisterState.js @@ -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) { + } +} diff --git a/src/services/request.js b/src/services/request.js index d9f7c4f..e2c0ce8 100644 --- a/src/services/request.js +++ b/src/services/request.js @@ -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) ; },