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 {
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() {

View File

@@ -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}>

View File

@@ -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 = {};

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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() {

View File

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

View File

@@ -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>

View File

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

View File

@@ -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;
}
}

View File

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