Auth flow. The next

This commit is contained in:
SleepWalker 2016-03-01 22:36:14 +02:00
parent a317bfd3d4
commit 57f0cf30e6
28 changed files with 438 additions and 243 deletions

View File

@ -13,7 +13,6 @@ import messages from './Activation.messages';
class Body extends BaseAuthBody { class Body extends BaseAuthBody {
static propTypes = { static propTypes = {
...BaseAuthBody.propTypes, ...BaseAuthBody.propTypes,
activate: PropTypes.func.isRequired,
auth: PropTypes.shape({ auth: PropTypes.shape({
error: PropTypes.string, error: PropTypes.string,
login: PropTypes.shape({ login: PropTypes.shape({
@ -48,10 +47,6 @@ class Body extends BaseAuthBody {
</div> </div>
); );
} }
onFormSubmit() {
this.props.activate(this.serialize());
}
} }
export default function Activation() { export default function Activation() {

View File

@ -22,7 +22,7 @@ export default class AppInfo extends Component {
return ( return (
<div className={styles.appInfo}> <div className={styles.appInfo}>
<div className={styles.logoContainer}> <div className={styles.logoContainer}>
<h2 className={styles.logo}>{name}</h2> <h2 className={styles.logo}>{name || 'Ely Accounts'}</h2>
</div> </div>
<div className={styles.descriptionContainer}> <div className={styles.descriptionContainer}>
<p className={styles.description}> <p className={styles.description}>

View File

@ -8,6 +8,8 @@ import AuthError from './AuthError';
export default class BaseAuthBody extends Component { export default class BaseAuthBody extends Component {
static propTypes = { static propTypes = {
clearErrors: PropTypes.func.isRequired, clearErrors: PropTypes.func.isRequired,
resolve: PropTypes.func.isRequired,
reject: PropTypes.func.isRequired,
auth: PropTypes.shape({ auth: PropTypes.shape({
error: PropTypes.string error: PropTypes.string
}) })
@ -20,6 +22,10 @@ export default class BaseAuthBody extends Component {
; ;
} }
onFormSubmit() {
this.props.resolve(this.serialize());
}
onClearErrors = this.props.clearErrors; onClearErrors = this.props.clearErrors;
form = {}; form = {};

View File

@ -14,7 +14,6 @@ import passwordMessages from './Password.messages';
class Body extends BaseAuthBody { class Body extends BaseAuthBody {
static propTypes = { static propTypes = {
...BaseAuthBody.propTypes, ...BaseAuthBody.propTypes,
login: PropTypes.func.isRequired,
auth: PropTypes.shape({ auth: PropTypes.shape({
error: PropTypes.string, error: PropTypes.string,
login: PropTypes.shape({ login: PropTypes.shape({
@ -37,10 +36,6 @@ class Body extends BaseAuthBody {
</div> </div>
); );
} }
onFormSubmit() {
this.props.login(this.serialize());
}
} }
export default function Login() { export default function Login() {

View File

@ -1,25 +1,9 @@
import React, { Component, PropTypes } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; export class Logout extends Component {
import { logout } from 'components/auth/actions';
class Logout extends Component {
static displayName = 'Logout'; static displayName = 'Logout';
static propTypes = {
logout: PropTypes.func.isRequired
};
componentWillMount() {
this.props.logout();
}
render() { render() {
return <span />; return <span/>;
} }
} }
export default connect(null, {
logout
})(Logout);

View File

@ -1,43 +1,9 @@
import React, { Component, PropTypes } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; export default class OAuthInit extends Component {
import { oAuthValidate, oAuthComplete } from 'components/auth/actions';
class OAuthInit extends Component {
static displayName = 'OAuthInit'; 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() { render() {
return <span />; return <span />;
} }
} }
export default connect((state) => ({
query: state.routing.location.query
}), {
validate: oAuthValidate,
complete: oAuthComplete
})(OAuthInit);

View File

@ -1,7 +1,6 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { routeActions } from 'react-router-redux';
import { TransitionMotion, spring } from 'react-motion'; import { TransitionMotion, spring } from 'react-motion';
import ReactHeight from 'react-height'; 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 {helpLinks as helpLinksStyles} from 'components/auth/helpLinks.scss';
import panelStyles from 'components/ui/panel.scss'; import panelStyles from 'components/ui/panel.scss';
import icons from 'components/ui/icons.scss'; import icons from 'components/ui/icons.scss';
import authFlow from 'services/authFlow';
import * as actions from './actions'; import * as actions from './actions';
@ -28,7 +28,6 @@ class PanelTransition extends Component {
password: PropTypes.string password: PropTypes.string
}) })
}).isRequired, }).isRequired,
goBack: React.PropTypes.func.isRequired,
setError: React.PropTypes.func.isRequired, setError: React.PropTypes.func.isRequired,
clearErrors: React.PropTypes.func.isRequired, clearErrors: React.PropTypes.func.isRequired,
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,
@ -211,8 +210,7 @@ class PanelTransition extends Component {
onGoBack = (event) => { onGoBack = (event) => {
event.preventDefault(); event.preventDefault();
this.body.onGoBack && this.body.onGoBack(); authFlow.goBack();
this.props.goBack();
}; };
getHeader(key, props) { getHeader(key, props) {
@ -341,14 +339,10 @@ class PanelTransition extends Component {
export default connect((state) => ({ export default connect((state) => ({
user: state.user, user: state.user,
auth: state.auth, 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, clearErrors: actions.clearErrors,
oAuthComplete: actions.oAuthComplete,
setError: actions.setError setError: actions.setError
})(PanelTransition); })(PanelTransition);

View File

@ -15,8 +15,6 @@ import messages from './Password.messages';
class Body extends BaseAuthBody { class Body extends BaseAuthBody {
static propTypes = { static propTypes = {
...BaseAuthBody.propTypes, ...BaseAuthBody.propTypes,
login: PropTypes.func.isRequired,
logout: PropTypes.func.isRequired,
auth: PropTypes.shape({ auth: PropTypes.shape({
error: PropTypes.string, error: PropTypes.string,
login: PropTypes.shape({ login: PropTypes.shape({
@ -56,17 +54,6 @@ class Body extends BaseAuthBody {
</div> </div>
); );
} }
onFormSubmit() {
this.props.login({
...this.serialize(),
login: this.props.user.email || this.props.user.username
});
}
onGoBack() {
this.props.logout();
}
} }
export default function Password() { export default function Password() {

View File

@ -15,15 +15,7 @@ import styles from './passwordChange.scss';
class Body extends BaseAuthBody { class Body extends BaseAuthBody {
static propTypes = { static propTypes = {
...BaseAuthBody.propTypes/*, ...BaseAuthBody.propTypes
// Я так полагаю, это правила валидации?
login: PropTypes.func.isRequired,
auth: PropTypes.shape({
error: PropTypes.string,
login: PropTypes.shape({
login: PropTypes.stirng
})
})*/
}; };
render() { render() {
@ -56,10 +48,6 @@ class Body extends BaseAuthBody {
</div> </div>
); );
} }
onFormSubmit() {
this.props.login(this.serialize());
}
} }
export default function PasswordChange() { export default function PasswordChange() {
@ -75,10 +63,14 @@ export default function PasswordChange() {
<Message {...passwordChangedMessages.change} /> <Message {...passwordChangedMessages.change} />
</button> </button>
), ),
Links: () => ( Links: (props) => (
<Link to="/oauth/permissions"> <a href="#" onClick={(event) => {
event.preventDefault();
props.reject();
}}>
<Message {...passwordChangedMessages.skipThisStep} /> <Message {...passwordChangedMessages.skipThisStep} />
</Link> </a>
) )
}; };
} }

View File

@ -14,8 +14,6 @@ import messages from './Permissions.messages';
class Body extends BaseAuthBody { class Body extends BaseAuthBody {
static propTypes = { static propTypes = {
...BaseAuthBody.propTypes, ...BaseAuthBody.propTypes,
login: PropTypes.func.isRequired,
oAuthComplete: PropTypes.func.isRequired,
auth: PropTypes.shape({ auth: PropTypes.shape({
error: PropTypes.string, error: PropTypes.string,
scopes: PropTypes.array.isRequired scopes: PropTypes.array.isRequired
@ -52,20 +50,14 @@ class Body extends BaseAuthBody {
<Message {...messages.theAppNeedsAccess2} /> <Message {...messages.theAppNeedsAccess2} />
</div> </div>
<ul className={styles.permissionsList}> <ul className={styles.permissionsList}>
{scopes.map((scope) => ( {scopes.map((scope, key) => (
<li>{<Message {...messages[`scope_${scope}`]} />}</li> <li key={key}>{<Message {...messages[`scope_${scope}`]} />}</li>
))} ))}
</ul> </ul>
</div> </div>
</div> </div>
); );
} }
onFormSubmit() {
this.props.oAuthComplete({
accept: true
});
}
} }
export default function Permissions() { export default function Permissions() {
@ -85,9 +77,7 @@ export default function Permissions() {
<a href="#" onClick={(event) => { <a href="#" onClick={(event) => {
event.preventDefault(); event.preventDefault();
props.onAuthComplete({ props.reject();
accept: false
});
}}> }}>
<Message {...messages.decline} /> <Message {...messages.decline} />
</a> </a>

View File

@ -82,10 +82,6 @@ class Body extends BaseAuthBody {
</div> </div>
); );
} }
onFormSubmit() {
this.props.register(this.serialize());
}
} }
export default function Register() { export default function Register() {

View File

@ -19,30 +19,26 @@ export function login({login = '', password = '', rememberMe = false}) {
token: resp.jwt token: resp.jwt
})); }));
dispatch(authenticate(resp.jwt)); return dispatch(authenticate(resp.jwt));
dispatch(redirectToGoal());
}) })
.catch((resp) => { .catch((resp) => {
if (resp.errors.login === ACTIVATION_REQUIRED) { if (resp.errors.login === ACTIVATION_REQUIRED) {
dispatch(updateUser({ return dispatch(updateUser({
isActive: false, isActive: false,
isGuest: false isGuest: false
})); }));
dispatch(redirectToGoal());
} else if (resp.errors.password === PASSWORD_REQUIRED) { } else if (resp.errors.password === PASSWORD_REQUIRED) {
dispatch(updateUser({ return dispatch(updateUser({
username: login, username: login,
email: login email: login
})); }));
dispatch(routeActions.push('/password'));
} else { } else {
if (resp.errors.login === LOGIN_REQUIRED && password) { if (resp.errors.login === LOGIN_REQUIRED && password) {
dispatch(logout()); dispatch(logout());
} }
const errorMessage = resp.errors[Object.keys(resp.errors)[0]]; const errorMessage = resp.errors[Object.keys(resp.errors)[0]];
dispatch(setError(errorMessage)); dispatch(setError(errorMessage));
throw new Error(errorMessage);
} }
// TODO: log unexpected errors // TODO: log unexpected errors
@ -73,6 +69,7 @@ export function register({
.catch((resp) => { .catch((resp) => {
const errorMessage = resp.errors[Object.keys(resp.errors)[0]]; const errorMessage = resp.errors[Object.keys(resp.errors)[0]];
dispatch(setError(errorMessage)); dispatch(setError(errorMessage));
throw new Error(errorMessage);
// TODO: log unexpected errors // TODO: log unexpected errors
}) })
@ -87,43 +84,22 @@ export function activate({key = ''}) {
) )
.then((resp) => { .then((resp) => {
dispatch(updateUser({ dispatch(updateUser({
isGuest: false,
isActive: true isActive: true
})); }));
dispatch(authenticate(resp.jwt)); return dispatch(authenticate(resp.jwt));
dispatch(redirectToGoal());
}) })
.catch((resp) => { .catch((resp) => {
const errorMessage = resp.errors[Object.keys(resp.errors)[0]]; const errorMessage = resp.errors[Object.keys(resp.errors)[0]];
dispatch(setError(errorMessage)); dispatch(setError(errorMessage));
throw new Error(errorMessage);
// TODO: log unexpected errors // 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 const ERROR = 'error';
export function setError(error) { export function setError(error) {
return { return {
@ -138,10 +114,7 @@ export function clearErrors() {
} }
export function logout() { export function logout() {
return (dispatch) => { return logoutUser();
dispatch(logoutUser());
dispatch(routeActions.push('/login'));
};
} }
// TODO: move to oAuth actions? // TODO: move to oAuth actions?
@ -174,28 +147,26 @@ export function oAuthComplete(params = {}) {
`/api/oauth/complete?${query}`, `/api/oauth/complete?${query}`,
typeof params.accept === 'undefined' ? {} : {accept: params.accept} 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 .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') { if (resp.statusCode === 401 && resp.error === 'access_denied') {
// user declined permissions // 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 = {}) { function handleOauthParamsValidation(resp = {}) {
const error = new Error('Error completing request');
if (resp.statusCode === 400 && resp.error === 'invalid_request') { if (resp.statusCode === 400 && resp.error === 'invalid_request') {
alert(`Invalid request (${resp.parameter} required).`); alert(`Invalid request (${resp.parameter} required).`);
throw error;
} }
if (resp.statusCode === 400 && resp.error === 'unsupported_response_type') { if (resp.statusCode === 400 && resp.error === 'unsupported_response_type') {
alert(`Invalid response type '${resp.parameter}'.`); alert(`Invalid response type '${resp.parameter}'.`);
throw error;
} }
if (resp.statusCode === 400 && resp.error === 'invalid_scope') { if (resp.statusCode === 400 && resp.error === 'invalid_scope') {
alert(`Invalid scope '${resp.parameter}'.`); alert(`Invalid scope '${resp.parameter}'.`);
throw error;
} }
if (resp.statusCode === 401 && resp.error === 'invalid_client') { if (resp.statusCode === 401 && resp.error === 'invalid_client') {
alert('Can not find application you are trying to authorize.'); alert('Can not find application you are trying to authorize.');
throw error;
} }
} }

View File

@ -53,6 +53,6 @@ export function authenticate(token) {
return (dispatch) => { return (dispatch) => {
request.setAuthToken(token); request.setAuthToken(token);
dispatch(fetchUserData()); return dispatch(fetchUserData());
}; };
} }

View File

@ -1,16 +1,32 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import AuthPage from 'pages/auth/AuthPage'; import { connect } from 'react-redux';
import Login from 'components/auth/Login';
export default class IndexPage extends Component { import authFlow from 'services/authFlow';
class IndexPage extends Component {
displayName = 'IndexPage'; displayName = 'IndexPage';
componentWillMount() {
if (this.props.user.isGuest) {
authFlow.login();
}
}
render() { render() {
const {user, children} = this.props;
return ( return (
<AuthPage> <div>
<Login /> <h1>
</AuthPage> Hello {user.username}!
</h1>
{children}
</div>
); );
} }
} }
export default connect((state) => ({
user: state.user
}))(IndexPage);

View File

@ -5,7 +5,7 @@ import RootPage from 'pages/root/RootPage';
import IndexPage from 'pages/index/IndexPage'; import IndexPage from 'pages/index/IndexPage';
import AuthPage from 'pages/auth/AuthPage'; 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 OAuthInit from 'components/auth/OAuthInit';
import Register from 'components/auth/Register'; import Register from 'components/auth/Register';
@ -17,72 +17,37 @@ import Logout from 'components/auth/Logout';
import PasswordChange from 'components/auth/PasswordChange'; import PasswordChange from 'components/auth/PasswordChange';
import ForgotPassword from 'components/auth/ForgotPassword'; import ForgotPassword from 'components/auth/ForgotPassword';
import authFlow from 'services/authFlow';
export default function routesFactory(store) { 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(); const state = store.getState();
if (state.user.token) { if (state.user.token) {
// authorizing user if it is possible // authorizing user if it is possible
store.dispatch(authenticate(state.user.token)); store.dispatch(authenticate(state.user.token));
} }
authFlow.setStore(store);
const onEnter = {
onEnter: ({location}, replace) => authFlow.handleRequest(location.pathname, replace)
};
return ( return (
<Route path="/" component={RootPage}> <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="auth" component={AuthPage}>
<Route path="/login" components={new Login()} onEnter={checkAuth} /> <Route path="/login" components={new Login()} {...onEnter} />
<Route path="/password" components={new Password()} onEnter={checkAuth} /> <Route path="/password" components={new Password()} {...onEnter} />
<Route path="/register" components={new Register()} /> <Route path="/register" components={new Register()} {...onEnter} />
<Route path="/activation" components={new Activation()} /> <Route path="/activation" components={new Activation()} {...onEnter} />
<Route path="/oauth/permissions" components={new Permissions()} onEnter={checkAuth} /> <Route path="/oauth/permissions" components={new Permissions()} {...onEnter} />
<Route path="/password-change" components={new PasswordChange()} /> <Route path="/password-change" components={new PasswordChange()} {...onEnter} />
<Route path="/forgot-password" components={new ForgotPassword()} /> <Route path="/forgot-password" components={new ForgotPassword()} {...onEnter} />
</Route> </Route>
<Route path="oauth" component={OAuthInit} />
<Route path="logout" component={Logout} />
</Route> </Route>
); );
} }

5
src/services/authFlow.js Normal file
View 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();

View File

@ -0,0 +1,9 @@
export default class AbstractState {
resolve() {}
goBack() {
throw new Error('There is no way back');
}
reject() {}
enter() {}
leave() {}
}

View 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()));
}
}

View 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());
}
}

View 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());
}
}

View 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('/');
}
}
}

View 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');
}
}

View 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());
}
}

View 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()));
}
}

View 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());
}
}

View 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);
}
}

View 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) {
}
}

View File

@ -28,9 +28,11 @@ function buildQuery(data) {
let authToken; let authToken;
const checkStatus = (resp) => Promise[resp.status >= 200 && resp.status < 300 ? 'resolve' : 'reject'](resp);
const toJSON = (resp) => resp.json(); 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 handleResponse = (resp) => Promise[resp.success || typeof resp.success === 'undefined' ? 'resolve' : 'reject'](resp);
const getDefaultHeaders = () => { const getDefaultHeaders = () => {
const header = {Accept: 'application/json'}; const header = {Accept: 'application/json'};
@ -51,7 +53,8 @@ export default {
}, },
body: buildQuery(data) body: buildQuery(data)
}) })
.then(toJSON) .then(checkStatus)
.then(toJSON, rejectWithJSON)
.then(handleResponse) .then(handleResponse)
; ;
}, },
@ -65,7 +68,8 @@ export default {
return fetch(url, { return fetch(url, {
headers: getDefaultHeaders() headers: getDefaultHeaders()
}) })
.then(toJSON) .then(checkStatus)
.then(toJSON, rejectWithJSON)
.then(handleResponse) .then(handleResponse)
; ;
}, },