mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-05-31 14:11:58 +05:30
#48: initial logic for multy-accounts actions
This commit is contained in:
97
src/components/accounts/actions.js
Normal file
97
src/components/accounts/actions.js
Normal 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
|
||||
};
|
||||
}
|
||||
58
src/components/accounts/reducer.js
Normal file
58
src/components/accounts/reducer.js
Normal 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: []
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user