accounts-frontend/src/components/auth/actions.js

541 lines
14 KiB
JavaScript
Raw Normal View History

2017-08-23 00:09:08 +05:30
// @flow
import { browserHistory } from 'services/history';
import logger from 'services/logger';
import localStorage from 'services/localStorage';
import loader from 'services/loader';
import history from 'services/history';
import { updateUser, acceptRules as userAcceptRules } from 'components/user/actions';
import { authenticate, logoutAll } from 'components/accounts/actions';
import authentication from 'services/api/authentication';
import oauth from 'services/api/oauth';
2016-07-28 10:33:30 +05:30
import signup from 'services/api/signup';
2016-08-07 20:20:00 +05:30
import dispatchBsod from 'components/ui/bsod/dispatchBsod';
import { create as createPopup } from 'components/ui/popup/actions';
import ContactForm from 'components/contact/ContactForm';
export { updateUser } from 'components/user/actions';
export { authenticate, logoutAll as logout } from 'components/accounts/actions';
/**
* Reoutes user to the previous page if it is possible
*
* @param {string} fallbackUrl - an url to route user to if goBack is not possible
*
* @return {object} - action definition
*/
2017-08-23 00:09:08 +05:30
export function goBack(fallbackUrl?: ?string = null) {
if (history.canGoBack()) {
browserHistory.goBack();
} else if (fallbackUrl) {
browserHistory.push(fallbackUrl);
}
return {
type: 'noop'
};
}
2017-09-09 19:52:19 +05:30
export function redirect(url: string): () => Promise<*> {
loader.show();
return () => new Promise(() => {
// do not resolve promise to make loader visible and
// overcome app rendering
location.href = url;
});
}
2017-08-23 00:09:08 +05:30
export function login({
login = '',
password = '',
totp,
rememberMe = false
}: {
login: string,
password?: string,
totp?: string,
rememberMe?: bool
}) {
const PASSWORD_REQUIRED = 'error.password_required';
const LOGIN_REQUIRED = 'error.login_required';
const ACTIVATION_REQUIRED = 'error.account_not_activated';
2017-08-23 00:09:08 +05:30
const TOTP_REQUIRED = 'error.totp_required';
return wrapInLoader((dispatch) =>
authentication.login(
2017-08-23 00:09:08 +05:30
{login, password, totp, rememberMe}
)
.then(authHandler(dispatch))
.catch((resp) => {
if (resp.errors) {
if (resp.errors.password === PASSWORD_REQUIRED) {
return dispatch(setLogin(login));
} else if (resp.errors.login === ACTIVATION_REQUIRED) {
return dispatch(needActivation());
} else if (resp.errors.totp === TOTP_REQUIRED) {
return dispatch(requestTotp({
login,
password,
rememberMe
}));
} else if (resp.errors.login === LOGIN_REQUIRED && password) {
logger.warn('No login on password panel');
return dispatch(logoutAll());
}
}
return validationErrorsHandler(dispatch)(resp);
})
);
}
2016-08-03 00:29:29 +05:30
export function acceptRules() {
return wrapInLoader((dispatch) =>
dispatch(userAcceptRules())
.catch(validationErrorsHandler(dispatch))
);
}
export function forgotPassword({
login = '',
captcha = ''
2017-08-23 00:09:08 +05:30
}: {
login: string,
captcha: string
}) {
return wrapInLoader((dispatch, getState) =>
authentication.forgotPassword({login, captcha})
2016-07-28 10:33:30 +05:30
.then(({data = {}}) => dispatch(updateUser({
maskedEmail: data.emailMask || getState().user.email
})))
.catch(validationErrorsHandler(dispatch))
);
}
export function recoverPassword({
key = '',
newPassword = '',
newRePassword = ''
2017-08-23 00:09:08 +05:30
}: {
key: string,
newPassword: string,
newRePassword: string
}) {
return wrapInLoader((dispatch) =>
2016-07-28 10:33:30 +05:30
authentication.recoverPassword({key, newPassword, newRePassword})
.then(authHandler(dispatch))
.catch(validationErrorsHandler(dispatch, '/forgot-password'))
);
}
export function register({
email = '',
username = '',
password = '',
rePassword = '',
2016-08-05 11:13:46 +05:30
captcha = '',
rulesAgreement = false
2017-08-23 00:09:08 +05:30
}: {
email: string,
username: string,
password: string,
rePassword: string,
captcha: string,
rulesAgreement: bool
}) {
return wrapInLoader((dispatch, getState) =>
2016-07-28 10:33:30 +05:30
signup.register({
email, username,
password, rePassword,
2016-08-05 11:13:46 +05:30
rulesAgreement, lang: getState().user.lang,
captcha
2016-07-28 10:33:30 +05:30
})
.then(() => {
dispatch(updateUser({
username,
email
}));
dispatch(needActivation());
browserHistory.push('/activation');
})
.catch(validationErrorsHandler(dispatch))
);
}
2017-08-23 00:09:08 +05:30
export function activate({key = ''}: {key: string}) {
return wrapInLoader((dispatch) =>
2016-07-28 10:33:30 +05:30
signup.activate({key})
.then(authHandler(dispatch))
.catch(validationErrorsHandler(dispatch, '/resend-activation'))
);
}
2017-08-23 00:09:08 +05:30
export function resendActivation({
email = '',
captcha
}: {
email: string,
captcha: string
}) {
return wrapInLoader((dispatch) =>
signup.resendActivation({email, captcha})
2016-07-28 10:33:30 +05:30
.then((resp) => {
dispatch(updateUser({
email
}));
2016-07-28 10:33:30 +05:30
return resp;
})
.catch(validationErrorsHandler(dispatch))
);
}
export function contactUs() {
return createPopup(ContactForm);
}
2017-08-23 00:09:08 +05:30
export const SET_CREDENTIALS = 'auth:setCredentials';
/**
* Sets login in credentials state
*
* Resets the state, when `null` is passed
*
* @param {string|null} login
*
* @return {object}
*/
export function setLogin(login: ?string) {
return {
type: SET_CREDENTIALS,
payload: login ? {
login
} : null
};
}
function requestTotp({login, password, rememberMe}: {
login: string,
password: string,
rememberMe: bool
}) {
return {
2017-08-23 00:09:08 +05:30
type: SET_CREDENTIALS,
payload: {
login,
password,
rememberMe,
isTotpRequired: true
}
};
}
2016-11-13 20:17:56 +05:30
export const SET_SWITCHER = 'auth:setAccountSwitcher';
2017-08-23 00:09:08 +05:30
export function setAccountSwitcher(isOn: bool) {
2016-11-13 20:17:56 +05:30
return {
type: SET_SWITCHER,
payload: isOn
};
}
export const ERROR = 'auth:error';
2017-08-23 00:09:08 +05:30
export function setErrors(errors: ?{[key: string]: string}) {
return {
type: ERROR,
payload: errors,
error: true
};
}
export function clearErrors() {
return setErrors(null);
}
const KNOWN_SCOPES = [
'minecraft_server_session',
'offline_access',
'account_info',
'account_email',
];
/**
* @param {object} oauthData
* @param {string} oauthData.clientId
* @param {string} oauthData.redirectUrl
* @param {string} oauthData.responseType
* @param {string} oauthData.description
* @param {string} oauthData.scope
* @param {string} [oauthData.prompt='none'] - comma-separated list of values to adjust auth flow
* Posible values:
* * none - default behaviour
* * consent - forcibly prompt user for rules acceptance
* * select_account - force account choosage, even if user has only one
* @param {string} oauthData.loginHint - allows to choose the account, which will be used for auth
* The possible values: account id, email, username
* @param {string} oauthData.state
*
* @return {Promise}
*/
2017-08-23 00:09:08 +05:30
export function oAuthValidate(oauthData: {
clientId: string,
redirectUrl: string,
responseType: string,
description: string,
scope: string,
prompt: 'none'|'consent'|'select_account',
loginHint?: string,
state?: string
}) {
// TODO: move to oAuth actions?
// test request: /oauth?client_id=ely&redirect_uri=http%3A%2F%2Fely.by&response_type=code&scope=minecraft_server_session&description=foo
return wrapInLoader((dispatch) =>
oauth.validate(oauthData)
.then((resp) => {
const scopes = resp.session.scopes;
const invalidScopes = scopes.filter((scope) => !KNOWN_SCOPES.includes(scope));
let prompt = (oauthData.prompt || 'none').split(',').map((item) => item.trim);
if (prompt.includes('none')) {
prompt = ['none'];
}
if (invalidScopes.length) {
logger.error('Got invalid scopes after oauth validation', {
invalidScopes
});
}
dispatch(setClient(resp.client));
dispatch(setOAuthRequest({
...resp.oAuth,
prompt: oauthData.prompt || 'none',
loginHint: oauthData.loginHint
}));
dispatch(setScopes(scopes));
localStorage.setItem('oauthData', JSON.stringify({ // @see services/authFlow/AuthFlow
timestamp: Date.now(),
payload: oauthData
}));
})
.catch(handleOauthParamsValidation)
);
2016-02-27 16:23:58 +05:30
}
2016-08-27 15:49:02 +05:30
/**
* @param {object} params
* @param {bool} params.accept=false
*
* @return {Promise}
*/
2017-08-23 00:09:08 +05:30
export function oAuthComplete(params: {accept?: bool} = {}) {
return wrapInLoader((dispatch, getState) =>
oauth.complete(getState().auth.oauth, params)
.then((resp) => {
2016-08-27 15:49:02 +05:30
localStorage.removeItem('oauthData');
if (resp.redirectUri.startsWith('static_page')) {
resp.code = resp.redirectUri.match(/code=(.+)&/)[1];
resp.redirectUri = resp.redirectUri.match(/^(.+)\?/)[1];
resp.displayCode = resp.redirectUri === 'static_page_with_code';
dispatch(setOAuthCode({
success: resp.success,
code: resp.code,
displayCode: resp.displayCode
}));
}
return resp;
}, (resp) => {
if (resp.acceptRequired) {
dispatch(requirePermissionsAccept());
2016-02-27 16:23:58 +05:30
2016-08-07 20:20:00 +05:30
return Promise.reject(resp);
}
return handleOauthParamsValidation(resp);
})
);
2016-02-27 16:23:58 +05:30
}
function handleOauthParamsValidation(resp = {}) {
2016-08-07 20:20:00 +05:30
dispatchBsod();
2016-08-27 15:49:02 +05:30
localStorage.removeItem('oauthData');
2016-08-07 20:20:00 +05:30
// eslint-disable-next-line no-alert
resp.userMessage && setTimeout(() => alert(resp.userMessage), 500); // 500 ms to allow re-render
return Promise.reject(resp);
2016-02-23 11:27:16 +05:30
}
export const SET_CLIENT = 'set_client';
2017-08-23 00:09:08 +05:30
export function setClient({
id,
name,
description
}: {
id: string,
name: string,
description: string
}) {
2016-02-23 11:27:16 +05:30
return {
type: SET_CLIENT,
payload: {id, name, description}
};
}
2016-02-27 16:23:58 +05:30
2016-11-19 16:24:24 +05:30
export function resetOAuth() {
2017-08-23 00:09:08 +05:30
return (dispatch: (Function|Object) => void) => {
2016-11-19 16:24:24 +05:30
localStorage.removeItem('oauthData');
dispatch(setOAuthRequest({}));
};
}
/**
* Resets all temporary state related to auth
*
* @return {function}
*/
export function resetAuth() {
2017-08-23 00:09:08 +05:30
return (dispatch: (Function|Object) => void) => {
dispatch(setLogin(null));
2017-08-23 00:09:08 +05:30
dispatch(resetOAuth());
};
}
2016-02-27 16:23:58 +05:30
export const SET_OAUTH = 'set_oauth';
2017-08-23 00:09:08 +05:30
export function setOAuthRequest(oauth: {
client_id?: string,
redirect_uri?: string,
response_type?: string,
scope?: string,
prompt?: string,
loginHint?: string,
state?: string
}) {
2016-02-27 16:23:58 +05:30
return {
type: SET_OAUTH,
payload: {
clientId: oauth.client_id,
redirectUrl: oauth.redirect_uri,
responseType: oauth.response_type,
scope: oauth.scope,
prompt: oauth.prompt,
loginHint: oauth.loginHint,
2016-02-27 16:23:58 +05:30
state: oauth.state
}
};
}
export const SET_OAUTH_RESULT = 'set_oauth_result';
2017-08-23 00:09:08 +05:30
export function setOAuthCode(oauth: {
success: bool,
code: string,
displayCode: bool
}) {
return {
type: SET_OAUTH_RESULT,
payload: {
success: oauth.success,
code: oauth.code,
displayCode: oauth.displayCode
}
};
}
export const REQUIRE_PERMISSIONS_ACCEPT = 'require_permissions_accept';
export function requirePermissionsAccept() {
return {
type: REQUIRE_PERMISSIONS_ACCEPT
};
}
export const SET_SCOPES = 'set_scopes';
2017-08-23 00:09:08 +05:30
export function setScopes(scopes: Array<string>) {
if (!(scopes instanceof Array)) {
throw new Error('Scopes must be array');
}
return {
type: SET_SCOPES,
payload: scopes
};
}
export const SET_LOADING_STATE = 'set_loading_state';
2017-08-23 00:09:08 +05:30
export function setLoadingState(isLoading: bool) {
return {
type: SET_LOADING_STATE,
payload: isLoading
};
}
function wrapInLoader(fn) {
2017-08-23 00:09:08 +05:30
return (dispatch: (Function|Object) => void, getState: Object) => {
dispatch(setLoadingState(true));
const endLoading = () => dispatch(setLoadingState(false));
return Reflect.apply(fn, null, [dispatch, getState]).then((resp) => {
endLoading();
return resp;
}, (resp) => {
endLoading();
return Promise.reject(resp);
});
};
}
function needActivation() {
return updateUser({
isActive: false,
isGuest: false
});
}
function authHandler(dispatch) {
2016-11-05 15:41:41 +05:30
return (resp) => dispatch(authenticate({
token: resp.access_token,
refreshToken: resp.refresh_token
})).then((resp) => {
dispatch(setLogin(null));
return resp;
});
}
2017-09-09 19:52:19 +05:30
function validationErrorsHandler(dispatch: (Function | Object) => void, repeatUrl?: string) {
return (resp) => {
if (resp.errors) {
const firstError = Object.keys(resp.errors)[0];
const error = {
type: resp.errors[firstError],
payload: {
2017-08-23 00:09:08 +05:30
isGuest: true,
repeatUrl: ''
}
};
if (resp.data) {
// TODO: this should be formatted on backend
Object.assign(error.payload, resp.data);
}
if (['error.key_not_exists', 'error.key_expire'].includes(error.type) && repeatUrl) {
// TODO: this should be formatted on backend
2017-08-23 00:09:08 +05:30
error.payload.repeatUrl = repeatUrl;
}
resp.errors[firstError] = error;
dispatch(setErrors(resp.errors));
}
return Promise.reject(resp);
};
}