#48: initial logic for multy-accounts actions

This commit is contained in:
SleepWalker
2016-10-30 14:12:49 +02:00
parent 200a1f339f
commit 7dd58acede
13 changed files with 572 additions and 116 deletions

View File

@@ -0,0 +1,97 @@
import authentication from 'services/api/authentication';
import accounts from 'services/api/accounts';
import { updateUser, logout } from 'components/user/actions';
/**
* @typedef {object} Account
* @property {string} account.id
* @property {string} account.username
* @property {string} account.email
* @property {string} account.token
* @property {string} account.refreshToken
*/
/**
* @param {Account|object} account
* @param {string} account.token
* @param {string} account.refreshToken
*/
export function authenticate({token, refreshToken}) {
return (dispatch) => {
return authentication.validateToken({token, refreshToken})
.then(({token, refreshToken}) =>
accounts.current({token})
.then((user) => ({
user,
account: {
id: user.id,
username: user.username,
email: user.email,
token,
refreshToken
}
}))
)
.then(({user, account}) => {
dispatch(add(account));
dispatch(activate(account));
dispatch(updateUser(user));
return account;
});
};
}
/**
* @param {Account} account
*/
export function revoke(account) {
return (dispatch, getState) => {
dispatch(remove(account));
if (getState().accounts.length) {
return dispatch(authenticate(getState().accounts[0]));
} else {
return dispatch(logout());
}
};
}
export const ADD = 'accounts:add';
/**
* @api private
*
* @param {Account} account
*/
export function add(account) {
return {
type: ADD,
payload: account
};
}
export const REMOVE = 'accounts:remove';
/**
* @api private
*
* @param {Account} account
*/
export function remove(account) {
return {
type: REMOVE,
payload: account
};
}
export const ACTIVATE = 'accounts:activate';
/**
* @api private
*
* @param {Account} account
*/
export function activate(account) {
return {
type: ACTIVATE,
payload: account
};
}

View File

@@ -0,0 +1,58 @@
import { ADD, REMOVE, ACTIVATE } from './actions';
/**
* @typedef {AccountsState}
* @property {Account} active
* @property {Account[]} available
*/
/**
* @param {AccountsState} state
* @param {string} options.type
* @param {object} options.payload
*
* @return {AccountsState}
*/
export default function accounts(
state,
{type, payload = {}}
) {
switch (type) {
case ADD:
if (!payload || !payload.id || !payload.token || !payload.refreshToken) {
throw new Error('Invalid or empty payload passed for accounts.add');
}
if (!state.available.some((account) => account.id === payload.id)) {
state.available = state.available.concat(payload);
}
return state;
case ACTIVATE:
if (!payload || !payload.id || !payload.token || !payload.refreshToken) {
throw new Error('Invalid or empty payload passed for accounts.add');
}
return {
...state,
active: payload
};
case REMOVE:
if (!payload || !payload.id) {
throw new Error('Invalid or empty payload passed for accounts.remove');
}
return {
...state,
available: state.available.filter((account) => account.id !== payload.id)
};
default:
return {
active: null,
available: []
};
}
}

View File

@@ -36,7 +36,7 @@ export function login({login = '', password = '', rememberMe = false}) {
return dispatch(needActivation());
} else if (resp.errors.login === LOGIN_REQUIRED && password) {
// return to the first step
dispatch(logout());
return dispatch(logout());
}
}

View File

@@ -4,7 +4,7 @@ const KEY_USER = 'user';
export default class User {
/**
* @param {object|string|undefined} data plain object or jwt token or empty to load from storage
* @param {object} [data] - plain object or jwt token or empty to load from storage
*
* @return {User}
*/
@@ -18,8 +18,6 @@ export default class User {
const defaults = {
id: null,
uuid: null,
token: '',
refreshToken: '',
username: '',
email: '',
// will contain user's email or masked email
@@ -27,12 +25,18 @@ export default class User {
maskedEmail: '',
avatar: '',
lang: '',
goal: null, // the goal with wich user entered site
isGuest: true,
isActive: false,
shouldAcceptRules: false, // whether user need to review updated rules
passwordChangedAt: null,
hasMojangUsernameCollision: false,
// frontend app specific attributes
isGuest: true,
goal: null, // the goal with wich user entered site
// TODO: the following does not belongs here. Move it later
token: '',
refreshToken: '',
};
const user = Object.keys(defaults).reduce((user, key) => {

View File

@@ -8,14 +8,18 @@
*/
export default function bearerHeaderMiddleware({getState}) {
return {
before(data) {
const {token} = getState().user;
before(req) {
let {token} = getState().user;
if (token) {
data.options.headers.Authorization = `Bearer ${token}`;
if (req.options.token) {
token = req.options.token;
}
return data;
if (token) {
req.options.headers.Authorization = `Bearer ${token}`;
}
return req;
}
};
}

View File

@@ -12,12 +12,12 @@ import {updateUser, logout} from '../actions';
*/
export default function refreshTokenMiddleware({dispatch, getState}) {
return {
before(data) {
before(req) {
const {refreshToken, token} = getState().user;
const isRefreshTokenRequest = data.url.includes('refresh-token');
const isRefreshTokenRequest = req.url.includes('refresh-token');
if (!token || isRefreshTokenRequest) {
return data;
if (!token || isRefreshTokenRequest || req.options.autoRefreshToken === false) {
return req;
}
try {
@@ -25,33 +25,17 @@ export default function refreshTokenMiddleware({dispatch, getState}) {
const jwt = getJWTPayload(token);
if (jwt.exp - SAFETY_FACTOR < Date.now() / 1000) {
return requestAccessToken(refreshToken, dispatch).then(() => data);
return requestAccessToken(refreshToken, dispatch).then(() => req);
}
} catch (err) {
dispatch(logout());
}
return data;
return req;
},
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) {
catch(resp, req, restart) {
if (resp && resp.status === 401 && req.options.autoRefreshToken !== false) {
const {refreshToken} = getState().user;
if (resp.message === 'Token expired' && refreshToken) {
// request token and retry