mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-12-16 10:19:03 +05:30
Auth flow. The next
This commit is contained in:
parent
a317bfd3d4
commit
57f0cf30e6
@ -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)
|
||||
;
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user