mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-12-28 07:50:32 +05:30
Merge branch 'account_sessions'
This commit is contained in:
commit
d3c68e8847
@ -2,6 +2,7 @@ import { routeActions } from 'react-router-redux';
|
||||
|
||||
import { updateUser, logout as logoutUser, changePassword as changeUserPassword, authenticate } from 'components/user/actions';
|
||||
import request from 'services/request';
|
||||
import authentication from 'services/api/authentication';
|
||||
|
||||
export function login({login = '', password = '', rememberMe = false}) {
|
||||
const PASSWORD_REQUIRED = 'error.password_required';
|
||||
@ -9,28 +10,21 @@ export function login({login = '', password = '', rememberMe = false}) {
|
||||
const ACTIVATION_REQUIRED = 'error.account_not_activated';
|
||||
|
||||
return wrapInLoader((dispatch) =>
|
||||
request.post(
|
||||
'/api/authentication/login',
|
||||
authentication.login(
|
||||
{login, password, rememberMe}
|
||||
)
|
||||
.then((resp) => {
|
||||
dispatch(updateUser({
|
||||
isGuest: false,
|
||||
token: resp.jwt
|
||||
}));
|
||||
|
||||
return dispatch(authenticate(resp.jwt));
|
||||
})
|
||||
.then(authHandler(dispatch))
|
||||
.catch((resp) => {
|
||||
if (resp.errors.login === ACTIVATION_REQUIRED) {
|
||||
return dispatch(needActivation());
|
||||
} else if (resp.errors.password === PASSWORD_REQUIRED) {
|
||||
return dispatch(updateUser({
|
||||
username: login,
|
||||
email: login
|
||||
}));
|
||||
} else if (resp.errors) {
|
||||
if (resp.errors.login === LOGIN_REQUIRED && password) {
|
||||
if (resp.errors) {
|
||||
if (resp.errors.password === PASSWORD_REQUIRED) {
|
||||
return dispatch(updateUser({
|
||||
username: login,
|
||||
email: login
|
||||
}));
|
||||
} else if (resp.errors.login === ACTIVATION_REQUIRED) {
|
||||
return dispatch(needActivation());
|
||||
} else if (resp.errors.login === LOGIN_REQUIRED && password) {
|
||||
// return to the first step
|
||||
dispatch(logout());
|
||||
}
|
||||
}
|
||||
@ -76,14 +70,7 @@ export function recoverPassword({
|
||||
'/api/authentication/recover-password',
|
||||
{key, newPassword, newRePassword}
|
||||
)
|
||||
.then((resp) => {
|
||||
dispatch(updateUser({
|
||||
isGuest: false,
|
||||
isActive: true
|
||||
}));
|
||||
|
||||
return dispatch(authenticate(resp.jwt));
|
||||
})
|
||||
.then(authHandler(dispatch))
|
||||
.catch(validationErrorsHandler(dispatch, '/forgot-password'))
|
||||
);
|
||||
}
|
||||
@ -118,14 +105,7 @@ export function activate({key = ''}) {
|
||||
'/api/signup/confirm',
|
||||
{key}
|
||||
)
|
||||
.then((resp) => {
|
||||
dispatch(updateUser({
|
||||
isGuest: false,
|
||||
isActive: true
|
||||
}));
|
||||
|
||||
return dispatch(authenticate(resp.jwt));
|
||||
})
|
||||
.then(authHandler(dispatch))
|
||||
.catch(validationErrorsHandler(dispatch, '/resend-activation'))
|
||||
);
|
||||
}
|
||||
@ -341,6 +321,10 @@ function needActivation() {
|
||||
});
|
||||
}
|
||||
|
||||
function authHandler(dispatch) {
|
||||
return (resp) => dispatch(authenticate(resp.access_token, resp.refresh_token));
|
||||
}
|
||||
|
||||
function validationErrorsHandler(dispatch, repeatUrl) {
|
||||
return (resp) => {
|
||||
if (resp.errors) {
|
||||
|
@ -32,6 +32,7 @@ export default class PasswordBody extends BaseAuthBody {
|
||||
{user.email || user.username}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Input {...this.bindField('password')}
|
||||
icon="key"
|
||||
type="password"
|
||||
@ -40,7 +41,7 @@ export default class PasswordBody extends BaseAuthBody {
|
||||
/>
|
||||
|
||||
<Checkbox {...this.bindField('rememberMe')}
|
||||
defaultChecked={true}
|
||||
defaultChecked
|
||||
label={messages.rememberMe}
|
||||
/>
|
||||
</div>
|
||||
|
@ -19,6 +19,7 @@ export default class User {
|
||||
id: null,
|
||||
uuid: null,
|
||||
token: '',
|
||||
refreshToken: '',
|
||||
username: '',
|
||||
email: '',
|
||||
// will contain user's email or masked email
|
||||
|
@ -56,22 +56,11 @@ export function logout() {
|
||||
export function fetchUserData() {
|
||||
return (dispatch) =>
|
||||
accounts.current()
|
||||
.then((resp) => {
|
||||
dispatch(updateUser(resp));
|
||||
.then((resp) => {
|
||||
dispatch(updateUser(resp));
|
||||
|
||||
return dispatch(changeLang(resp.lang));
|
||||
});
|
||||
/*
|
||||
.catch((resp) => {
|
||||
{
|
||||
"name": "Unauthorized",
|
||||
"message": "You are requesting with an invalid credential.",
|
||||
"code": 0,
|
||||
"status": 401,
|
||||
"type": "yii\\web\\UnauthorizedHttpException"
|
||||
}
|
||||
});
|
||||
*/
|
||||
return dispatch(changeLang(resp.lang));
|
||||
});
|
||||
}
|
||||
|
||||
export function changePassword({
|
||||
@ -94,14 +83,138 @@ export function changePassword({
|
||||
;
|
||||
}
|
||||
|
||||
let middlewareAdded = false;
|
||||
export function authenticate(token, refreshToken) { // TODO: this action, probably, belongs to components/auth
|
||||
return (dispatch, getState) => {
|
||||
if (!middlewareAdded) {
|
||||
request.addMiddleware(tokenCheckMiddleware(dispatch, getState));
|
||||
request.addMiddleware(tokenApplyMiddleware(dispatch, getState));
|
||||
middlewareAdded = true;
|
||||
}
|
||||
|
||||
export function authenticate(token) {
|
||||
if (!token || token.split('.').length !== 3) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
refreshToken = refreshToken || getState().user.refreshToken;
|
||||
dispatch(updateUser({
|
||||
token,
|
||||
refreshToken
|
||||
}));
|
||||
|
||||
return (dispatch) => {
|
||||
request.setAuthToken(token);
|
||||
return dispatch(fetchUserData());
|
||||
return dispatch(fetchUserData()).then((resp) => {
|
||||
dispatch(updateUser({
|
||||
isGuest: false
|
||||
}));
|
||||
return resp;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
import authentication from 'services/api/authentication';
|
||||
function requestAccessToken(refreshToken, dispatch) {
|
||||
let promise;
|
||||
if (refreshToken) {
|
||||
promise = authentication.refreshToken(refreshToken);
|
||||
} else {
|
||||
promise = Promise.reject();
|
||||
}
|
||||
|
||||
return promise
|
||||
.then((resp) => dispatch(updateUser({
|
||||
token: resp.access_token
|
||||
})))
|
||||
.catch(() => dispatch(logout()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures, that all user's requests have fresh access token
|
||||
*
|
||||
* @param {Function} dispatch
|
||||
* @param {Function} getState
|
||||
*
|
||||
* @return {Object} middleware
|
||||
*/
|
||||
function tokenCheckMiddleware(dispatch, getState) {
|
||||
return {
|
||||
before(data) {
|
||||
const {isGuest, refreshToken, token} = getState().user;
|
||||
const isRefreshTokenRequest = data.url.includes('refresh-token');
|
||||
|
||||
if (isGuest || isRefreshTokenRequest || !token) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const SAFETY_FACTOR = 60; // ask new token earlier to overcome time dissynchronization problem
|
||||
const jwt = getJWTPayload(token);
|
||||
|
||||
if (jwt.exp - SAFETY_FACTOR < Date.now() / 1000) {
|
||||
return requestAccessToken(refreshToken, dispatch).then(() => data);
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
catch(resp, restart) {
|
||||
/*
|
||||
{
|
||||
"name": "Unauthorized",
|
||||
"message": "You are requesting with an invalid credential.",
|
||||
"code": 0,
|
||||
"status": 401,
|
||||
"type": "yii\\web\\UnauthorizedHttpException"
|
||||
}
|
||||
{
|
||||
"name": "Unauthorized",
|
||||
"message": "Token expired",
|
||||
"code": 0,
|
||||
"status": 401,
|
||||
"type": "yii\\web\\UnauthorizedHttpException"
|
||||
}
|
||||
*/
|
||||
if (resp && resp.status === 401) {
|
||||
const {refreshToken} = getState().user;
|
||||
if (resp.message === 'Token expired' && refreshToken) {
|
||||
// request token and retry
|
||||
return requestAccessToken(refreshToken, dispatch).then(restart);
|
||||
}
|
||||
|
||||
dispatch(logout());
|
||||
}
|
||||
|
||||
return Promise.reject(resp);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies Bearer header for all requests
|
||||
*
|
||||
* @param {Function} dispatch
|
||||
* @param {Function} getState
|
||||
*
|
||||
* @return {Object} middleware
|
||||
*/
|
||||
function tokenApplyMiddleware(dispatch, getState) {
|
||||
return {
|
||||
before(data) {
|
||||
const {token} = getState().user;
|
||||
|
||||
if (token) {
|
||||
data.options.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getJWTPayload(jwt) {
|
||||
const parts = (jwt || '').split('.');
|
||||
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('Invalid jwt token');
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(atob(parts[1]));
|
||||
} catch (err) {
|
||||
throw new Error('Can not decode jwt token');
|
||||
}
|
||||
}
|
||||
|
21
src/services/api/authentication.js
Normal file
21
src/services/api/authentication.js
Normal file
@ -0,0 +1,21 @@
|
||||
import request from 'services/request';
|
||||
|
||||
export default {
|
||||
login({
|
||||
login = '',
|
||||
password = '',
|
||||
rememberMe = false
|
||||
}) {
|
||||
return request.post(
|
||||
'/api/authentication/login',
|
||||
{login, password, rememberMe}
|
||||
);
|
||||
},
|
||||
|
||||
refreshToken(refresh_token) {
|
||||
return request.post(
|
||||
'/api/authentication/refresh-token',
|
||||
{refresh_token}
|
||||
);
|
||||
}
|
||||
};
|
@ -14,11 +14,12 @@ export default class PasswordState extends AbstractState {
|
||||
}
|
||||
}
|
||||
|
||||
resolve(context, {password}) {
|
||||
resolve(context, {password, rememberMe}) {
|
||||
const {user} = context.getState();
|
||||
|
||||
context.run('login', {
|
||||
password,
|
||||
rememberMe,
|
||||
login: user.email || user.username
|
||||
})
|
||||
.then(() => context.setState(new CompleteState()));
|
||||
|
@ -1,3 +1,73 @@
|
||||
const middlewares = [];
|
||||
|
||||
export default {
|
||||
post(url, data) {
|
||||
return doFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
},
|
||||
body: buildQuery(data)
|
||||
});
|
||||
},
|
||||
|
||||
get(url, data) {
|
||||
if (typeof data === 'object') {
|
||||
const separator = url.indexOf('?') === -1 ? '?' : '&';
|
||||
url += separator + buildQuery(data);
|
||||
}
|
||||
|
||||
return doFetch(url);
|
||||
},
|
||||
|
||||
buildQuery,
|
||||
|
||||
addMiddleware(middleware) {
|
||||
if (!middlewares.find((mdware) => mdware === middleware)) {
|
||||
middlewares.push(middleware);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const checkStatus = (resp) => Promise[resp.status >= 200 && resp.status < 300 ? 'resolve' : 'reject'](resp);
|
||||
const toJSON = (resp) => resp.json();
|
||||
const rejectWithJSON = (resp) => toJSON(resp).then((resp) => {throw resp;});
|
||||
const handleResponseSuccess = (resp) => Promise[resp.success || typeof resp.success === 'undefined' ? 'resolve' : 'reject'](resp);
|
||||
|
||||
function doFetch(url, options = {}) {
|
||||
// NOTE: we are wrapping fetch, because it is returning
|
||||
// Promise instance that can not be pollyfilled with Promise.prototype.finally
|
||||
|
||||
options.headers = options.headers || {};
|
||||
options.headers.Accept = 'application/json';
|
||||
|
||||
return runMiddlewares('before', {url, options})
|
||||
.then(({url, options}) => fetch(url, options))
|
||||
.then(checkStatus)
|
||||
.then(toJSON, rejectWithJSON)
|
||||
.then(handleResponseSuccess)
|
||||
.then((resp) => runMiddlewares('then', resp))
|
||||
.catch((resp) => runMiddlewares('catch', resp, () => doFetch(url, options)))
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} action - the name of middleware's hook (before|then|catch)
|
||||
* @param {Object} data - the initial data to pass through middlewares chain
|
||||
* @param {Function} restart - a function to restart current request (for `catch` hook)
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
function runMiddlewares(action, data, restart) {
|
||||
return middlewares
|
||||
.filter((middleware) => middleware[action])
|
||||
.reduce(
|
||||
(promise, middleware) => promise.then((resp) => middleware[action](resp, restart)),
|
||||
Promise[action === 'catch' ? 'reject' : 'resolve'](data)
|
||||
);
|
||||
}
|
||||
|
||||
function convertQueryValue(value) {
|
||||
if (typeof value === 'undefined') {
|
||||
return '';
|
||||
@ -25,64 +95,3 @@ function buildQuery(data = {}) {
|
||||
.join('&')
|
||||
;
|
||||
}
|
||||
|
||||
function doFetch(...args) {
|
||||
// NOTE: we are wrapping fetch, because it is returning
|
||||
// Promise instance that can not be pollyfilled with Promise.prototype.finally
|
||||
return new Promise((resolve, reject) => fetch(...args).then(resolve, reject));
|
||||
}
|
||||
|
||||
let authToken;
|
||||
|
||||
const checkStatus = (resp) => Promise[resp.status >= 200 && resp.status < 300 ? 'resolve' : 'reject'](resp);
|
||||
const toJSON = (resp) => resp.json();
|
||||
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'};
|
||||
|
||||
if (authToken) {
|
||||
header.Authorization = `Bearer ${authToken}`;
|
||||
}
|
||||
|
||||
return header;
|
||||
};
|
||||
|
||||
export default {
|
||||
post(url, data) {
|
||||
return doFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...getDefaultHeaders(),
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
},
|
||||
body: buildQuery(data)
|
||||
})
|
||||
.then(checkStatus)
|
||||
.then(toJSON, rejectWithJSON)
|
||||
.then(handleResponse)
|
||||
;
|
||||
},
|
||||
|
||||
get(url, data) {
|
||||
if (typeof data === 'object') {
|
||||
const separator = url.indexOf('?') === -1 ? '?' : '&';
|
||||
url += separator + buildQuery(data);
|
||||
}
|
||||
|
||||
return doFetch(url, {
|
||||
headers: getDefaultHeaders()
|
||||
})
|
||||
.then(checkStatus)
|
||||
.then(toJSON, rejectWithJSON)
|
||||
.then(handleResponse)
|
||||
;
|
||||
},
|
||||
|
||||
buildQuery,
|
||||
|
||||
setAuthToken(tkn) {
|
||||
authToken = tkn;
|
||||
}
|
||||
};
|
||||
|
@ -48,6 +48,7 @@ describe('PasswordState', () => {
|
||||
(function() {
|
||||
const expectedLogin = 'login';
|
||||
const expectedPassword = 'password';
|
||||
const expectedRememberMe = true;
|
||||
|
||||
const testWith = (user) => {
|
||||
it(`should call login with email or username and password. User: ${JSON.stringify(user)}`, () => {
|
||||
@ -58,11 +59,12 @@ describe('PasswordState', () => {
|
||||
'login',
|
||||
sinon.match({
|
||||
login: expectedLogin,
|
||||
password: expectedPassword
|
||||
password: expectedPassword,
|
||||
rememberMe: expectedRememberMe,
|
||||
})
|
||||
).returns({then() {}});
|
||||
|
||||
state.resolve(context, {password: expectedPassword});
|
||||
state.resolve(context, {password: expectedPassword, rememberMe: expectedRememberMe});
|
||||
});
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user