mirror of
https://github.com/elyby/accounts-frontend.git
synced 2025-05-31 14:11:58 +05:30
Create app namespace for all absolute requires of app modules. Move all packages under packages yarn workspace
This commit is contained in:
27
packages/app/components/user/User.ts
Normal file
27
packages/app/components/user/User.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* @typedef {object} User
|
||||
* @property {number} id
|
||||
* @property {string} uuid
|
||||
* @property {string} token
|
||||
* @property {string} username
|
||||
* @property {string} email
|
||||
* @property {string} avatar
|
||||
* @property {bool} isGuest
|
||||
* @property {bool} isActive
|
||||
* @property {number} passwordChangedAt - timestamp
|
||||
* @property {bool} hasMojangUsernameCollision
|
||||
*/
|
||||
export const userShape = PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
uuid: PropTypes.string,
|
||||
token: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
avatar: PropTypes.string,
|
||||
isGuest: PropTypes.bool.isRequired,
|
||||
isActive: PropTypes.bool.isRequired,
|
||||
passwordChangedAt: PropTypes.number,
|
||||
hasMojangUsernameCollision: PropTypes.bool,
|
||||
});
|
117
packages/app/components/user/actions.ts
Normal file
117
packages/app/components/user/actions.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
getInfo as getInfoEndpoint,
|
||||
changeLang as changeLangEndpoint,
|
||||
acceptRules as acceptRulesEndpoint,
|
||||
UserResponse,
|
||||
} from 'app/services/api/accounts';
|
||||
import { setLocale } from 'app/components/i18n/actions';
|
||||
import { ThunkAction } from 'app/reducers';
|
||||
|
||||
import { User } from './reducer';
|
||||
|
||||
export const UPDATE = 'USER_UPDATE';
|
||||
/**
|
||||
* Merge data into user's state
|
||||
*
|
||||
* @param {object} payload
|
||||
* @returns {object} - action definition
|
||||
*/
|
||||
export function updateUser(payload: Partial<User>) {
|
||||
// Temp workaround
|
||||
return {
|
||||
type: UPDATE,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export const SET = 'USER_SET';
|
||||
/**
|
||||
* Replace current user's state with a new one
|
||||
*
|
||||
* @param {User} payload
|
||||
* @returns {object} - action definition
|
||||
*/
|
||||
export function setUser(payload: Partial<User>) {
|
||||
return {
|
||||
type: SET,
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
export const CHANGE_LANG = 'USER_CHANGE_LANG';
|
||||
export function changeLang(lang: string): ThunkAction<Promise<void>> {
|
||||
return (dispatch, getState) =>
|
||||
dispatch(setLocale(lang)).then((lang: string) => {
|
||||
const { id, isGuest, lang: oldLang } = getState().user;
|
||||
|
||||
if (oldLang === lang) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isGuest && id) {
|
||||
changeLangEndpoint(id, lang);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: CHANGE_LANG,
|
||||
payload: {
|
||||
lang,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function setGuest(): ThunkAction<Promise<void>> {
|
||||
return async (dispatch, getState) => {
|
||||
dispatch(
|
||||
setUser({
|
||||
lang: getState().user.lang,
|
||||
isGuest: true,
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchUserData(): ThunkAction<Promise<UserResponse>> {
|
||||
return async (dispatch, getState) => {
|
||||
const { id } = getState().user;
|
||||
|
||||
if (!id) {
|
||||
throw new Error('Can not fetch user data. No user.id available');
|
||||
}
|
||||
|
||||
const resp = await getInfoEndpoint(id);
|
||||
|
||||
dispatch(
|
||||
updateUser({
|
||||
isGuest: false,
|
||||
...resp,
|
||||
}),
|
||||
);
|
||||
dispatch(changeLang(resp.lang));
|
||||
|
||||
return resp;
|
||||
};
|
||||
}
|
||||
|
||||
export function acceptRules(): ThunkAction<Promise<{ success: boolean }>> {
|
||||
return (dispatch, getState) => {
|
||||
const { id } = getState().user;
|
||||
|
||||
if (!id) {
|
||||
throw new Error(
|
||||
'user id is should be set at the moment when this action is called',
|
||||
);
|
||||
}
|
||||
|
||||
return acceptRulesEndpoint(id).then(resp => {
|
||||
dispatch(
|
||||
updateUser({
|
||||
shouldAcceptRules: false,
|
||||
}),
|
||||
);
|
||||
|
||||
return resp;
|
||||
});
|
||||
};
|
||||
}
|
50
packages/app/components/user/factory.ts
Normal file
50
packages/app/components/user/factory.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { authenticate, logoutStrangers } from 'app/components/accounts/actions';
|
||||
import { getActiveAccount } from 'app/components/accounts/reducer';
|
||||
import request from 'app/services/request';
|
||||
import { Store } from 'app/reducers';
|
||||
|
||||
import { changeLang } from './actions';
|
||||
import bearerHeaderMiddleware from './middlewares/bearerHeaderMiddleware';
|
||||
import refreshTokenMiddleware from './middlewares/refreshTokenMiddleware';
|
||||
|
||||
let promise: Promise<void>;
|
||||
|
||||
/**
|
||||
* Initializes User state with the fresh data
|
||||
*
|
||||
* @param {object} store - redux store
|
||||
*
|
||||
* @returns {Promise<User>} - a promise, that resolves in User state
|
||||
*/
|
||||
export function factory(store: Store): Promise<void> {
|
||||
if (promise) {
|
||||
return promise;
|
||||
}
|
||||
|
||||
request.addMiddleware(refreshTokenMiddleware(store));
|
||||
request.addMiddleware(bearerHeaderMiddleware(store));
|
||||
|
||||
promise = Promise.resolve()
|
||||
.then(() => store.dispatch(logoutStrangers()))
|
||||
.then(async () => {
|
||||
const activeAccount = getActiveAccount(store.getState());
|
||||
|
||||
if (activeAccount) {
|
||||
// authorizing user if it is possible
|
||||
await store.dispatch(authenticate(activeAccount));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.reject();
|
||||
})
|
||||
.catch(async () => {
|
||||
// the user is guest or user authentication failed
|
||||
const { user } = store.getState();
|
||||
|
||||
// auto-detect guest language
|
||||
await store.dispatch(changeLang(user.lang));
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
1
packages/app/components/user/index.ts
Normal file
1
packages/app/components/user/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { User } from './reducer';
|
@@ -0,0 +1,107 @@
|
||||
import expect from 'app/test/unexpected';
|
||||
import { RootState } from 'app/reducers';
|
||||
|
||||
import bearerHeaderMiddleware from 'app/components/user/middlewares/bearerHeaderMiddleware';
|
||||
|
||||
describe('bearerHeaderMiddleware', () => {
|
||||
const emptyState: RootState = {
|
||||
user: {},
|
||||
accounts: {
|
||||
active: null,
|
||||
available: [],
|
||||
},
|
||||
} as any;
|
||||
|
||||
describe('when token available', () => {
|
||||
const token = 'foo';
|
||||
const middleware = bearerHeaderMiddleware({
|
||||
getState: () => ({
|
||||
...emptyState,
|
||||
accounts: {
|
||||
active: 1,
|
||||
available: [
|
||||
{
|
||||
id: 1,
|
||||
token,
|
||||
username: 'username',
|
||||
email: 'email',
|
||||
refreshToken: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
} as any);
|
||||
|
||||
it('should set Authorization header', async () => {
|
||||
let data: any = {
|
||||
options: {
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
|
||||
data = middleware.before && (await middleware.before(data));
|
||||
|
||||
expectBearerHeader(data, token);
|
||||
});
|
||||
|
||||
it('overrides user.token with options.token if available', async () => {
|
||||
const tokenOverride = 'tokenOverride';
|
||||
let data: any = {
|
||||
options: {
|
||||
headers: {},
|
||||
token: tokenOverride,
|
||||
},
|
||||
};
|
||||
|
||||
data = middleware.before && (await middleware.before(data));
|
||||
|
||||
expectBearerHeader(data, tokenOverride);
|
||||
});
|
||||
|
||||
it('disables token if options.token is null', async () => {
|
||||
const tokenOverride = null;
|
||||
const data: any = {
|
||||
options: {
|
||||
headers: {} as { [key: string]: any },
|
||||
token: tokenOverride,
|
||||
},
|
||||
};
|
||||
|
||||
if (!middleware.before) {
|
||||
throw new Error('No middleware.before');
|
||||
}
|
||||
|
||||
const resp = await middleware.before(data);
|
||||
|
||||
expect(resp.options.headers.Authorization, 'to be undefined');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not set Authorization header if no token', async () => {
|
||||
const middleware = bearerHeaderMiddleware({
|
||||
getState: () => ({
|
||||
...emptyState,
|
||||
}),
|
||||
} as any);
|
||||
|
||||
const data: any = {
|
||||
options: {
|
||||
headers: {} as { [key: string]: any },
|
||||
},
|
||||
};
|
||||
|
||||
if (!middleware.before) {
|
||||
throw new Error('No middleware.before');
|
||||
}
|
||||
|
||||
const resp = await middleware.before(data);
|
||||
|
||||
expect(resp.options.headers.Authorization, 'to be undefined');
|
||||
});
|
||||
|
||||
function expectBearerHeader(data, token) {
|
||||
expect(data.options.headers, 'to satisfy', {
|
||||
Authorization: `Bearer ${token}`,
|
||||
});
|
||||
}
|
||||
});
|
@@ -0,0 +1,34 @@
|
||||
import { getActiveAccount } from 'app/components/accounts/reducer';
|
||||
import { Store } from 'app/reducers';
|
||||
import { Middleware } from 'app/services/request';
|
||||
|
||||
/**
|
||||
* Applies Bearer header for all requests
|
||||
*
|
||||
* req.options.token is used to override current token.
|
||||
* Pass null to disable bearer header at all
|
||||
*
|
||||
* @param {object} store - redux store
|
||||
* @param {Function} store.getState
|
||||
*
|
||||
* @returns {object} - request middleware
|
||||
*/
|
||||
export default function bearerHeaderMiddleware(store: Store): Middleware {
|
||||
return {
|
||||
async before(req) {
|
||||
const activeAccount = getActiveAccount(store.getState());
|
||||
|
||||
let { token = null } = activeAccount || {};
|
||||
|
||||
if (req.options.token || req.options.token === null) {
|
||||
({ token } = req.options);
|
||||
}
|
||||
|
||||
if (token) {
|
||||
req.options.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return req;
|
||||
},
|
||||
};
|
||||
}
|
@@ -0,0 +1,356 @@
|
||||
import expect from 'app/test/unexpected';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import refreshTokenMiddleware from 'app/components/user/middlewares/refreshTokenMiddleware';
|
||||
import { browserHistory } from 'app/services/history';
|
||||
import * as authentication from 'app/services/api/authentication';
|
||||
import { InternalServerError } from 'app/services/request';
|
||||
import { updateToken } from 'app/components/accounts/actions';
|
||||
|
||||
const refreshToken = 'foo';
|
||||
const expiredToken =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0NzA3NjE0NDMsImV4cCI6MTQ3MDc2MTQ0MywiaWF0IjoxNDcwNzYxNDQzLCJqdGkiOiJpZDEyMzQ1NiJ9.gWdnzfQQvarGpkbldUvB8qdJZSVkvdNtCbhbbl2yJW8';
|
||||
// valid till 2100 year
|
||||
const validToken =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0NzA3NjE5NzcsImV4cCI6NDEwMjQ0NDgwMCwiaWF0IjoxNDcwNzYxOTc3LCJqdGkiOiJpZDEyMzQ1NiJ9.M4KY4QgHOUzhpAZjWoHJbGsEJPR-RBsJ1c1BKyxvAoU';
|
||||
|
||||
describe('refreshTokenMiddleware', () => {
|
||||
let middleware;
|
||||
let getState;
|
||||
let dispatch;
|
||||
|
||||
const email = 'test@email.com';
|
||||
|
||||
beforeEach(() => {
|
||||
sinon
|
||||
.stub(authentication, 'requestToken')
|
||||
.named('authentication.requestToken');
|
||||
sinon.stub(authentication, 'logout').named('authentication.logout');
|
||||
sinon.stub(browserHistory, 'push');
|
||||
|
||||
getState = sinon.stub().named('store.getState');
|
||||
dispatch = sinon
|
||||
.spy(arg => (typeof arg === 'function' ? arg(dispatch, getState) : arg))
|
||||
.named('store.dispatch');
|
||||
|
||||
middleware = refreshTokenMiddleware({ getState, dispatch } as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(authentication.requestToken as any).restore();
|
||||
(authentication.logout as any).restore();
|
||||
(browserHistory.push as any).restore();
|
||||
});
|
||||
|
||||
function assertRelogin() {
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
{
|
||||
type: 'auth:setCredentials',
|
||||
payload: {
|
||||
login: email,
|
||||
returnUrl: expect.it('to be a string'),
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(browserHistory.push, 'to have a call satisfying', ['/login']);
|
||||
}
|
||||
|
||||
it('must be till 2100 to test with validToken', () =>
|
||||
expect(new Date().getFullYear(), 'to be less than', 2100));
|
||||
|
||||
describe('#before', () => {
|
||||
describe('when token expired', () => {
|
||||
beforeEach(() => {
|
||||
const account = {
|
||||
id: 42,
|
||||
email,
|
||||
token: expiredToken,
|
||||
refreshToken,
|
||||
};
|
||||
getState.returns({
|
||||
accounts: {
|
||||
active: account.id,
|
||||
available: [account],
|
||||
},
|
||||
auth: {
|
||||
credentials: {},
|
||||
},
|
||||
user: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should request new token', () => {
|
||||
const data = {
|
||||
url: 'foo',
|
||||
options: {
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
|
||||
(authentication.requestToken as any).returns(
|
||||
Promise.resolve(validToken),
|
||||
);
|
||||
|
||||
return middleware.before(data).then(resp => {
|
||||
expect(resp, 'to satisfy', data);
|
||||
|
||||
expect(authentication.requestToken, 'to have a call satisfying', [
|
||||
refreshToken,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not apply to refresh-token request', () => {
|
||||
const data = { url: '/refresh-token', options: {} };
|
||||
const resp = middleware.before(data);
|
||||
|
||||
return expect(resp, 'to be fulfilled with', data).then(() =>
|
||||
expect(authentication.requestToken, 'was not called'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not auto refresh token if options.token specified', () => {
|
||||
const data = {
|
||||
url: 'foo',
|
||||
options: { token: 'foo' },
|
||||
};
|
||||
middleware.before(data);
|
||||
|
||||
expect(authentication.requestToken, 'was not called');
|
||||
});
|
||||
|
||||
it('should update user with new token', () => {
|
||||
const data = {
|
||||
url: 'foo',
|
||||
options: {
|
||||
headers: {},
|
||||
},
|
||||
};
|
||||
|
||||
(authentication.requestToken as any).returns(
|
||||
Promise.resolve(validToken),
|
||||
);
|
||||
|
||||
return middleware
|
||||
.before(data)
|
||||
.then(() =>
|
||||
expect(dispatch, 'to have a call satisfying', [
|
||||
updateToken(validToken),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should relogin if token can not be parsed', () => {
|
||||
const account = {
|
||||
id: 42,
|
||||
email,
|
||||
token: 'realy bad token',
|
||||
refreshToken,
|
||||
};
|
||||
getState.returns({
|
||||
accounts: {
|
||||
active: account.id,
|
||||
available: [account],
|
||||
},
|
||||
auth: {
|
||||
credentials: {},
|
||||
},
|
||||
user: {},
|
||||
});
|
||||
|
||||
const req = { url: 'foo', options: {} };
|
||||
|
||||
return expect(middleware.before(req), 'to be rejected with', {
|
||||
message: 'Invalid token',
|
||||
}).then(() => {
|
||||
expect(authentication.requestToken, 'was not called');
|
||||
|
||||
assertRelogin();
|
||||
});
|
||||
});
|
||||
|
||||
it('should relogin if token request failed', () => {
|
||||
(authentication.requestToken as any).returns(Promise.reject());
|
||||
|
||||
return expect(
|
||||
middleware.before({ url: 'foo', options: {} }),
|
||||
'to be rejected',
|
||||
).then(() => assertRelogin());
|
||||
});
|
||||
|
||||
it('should not logout if request failed with 5xx', () => {
|
||||
const resp = new InternalServerError({}, { status: 500 });
|
||||
|
||||
(authentication.requestToken as any).returns(Promise.reject(resp));
|
||||
|
||||
return expect(
|
||||
middleware.before({ url: 'foo', options: {} }),
|
||||
'to be rejected with',
|
||||
resp,
|
||||
).then(() =>
|
||||
expect(dispatch, 'to have no calls satisfying', [
|
||||
{ payload: { isGuest: true } },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not be applied if no token', () => {
|
||||
getState.returns({
|
||||
accounts: {
|
||||
active: null,
|
||||
available: [],
|
||||
},
|
||||
user: {},
|
||||
});
|
||||
|
||||
const data = {
|
||||
url: 'foo',
|
||||
options: {},
|
||||
};
|
||||
const resp = middleware.before(data);
|
||||
|
||||
return expect(resp, 'to be fulfilled with', data).then(() =>
|
||||
expect(authentication.requestToken, 'was not called'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#catch', () => {
|
||||
const expiredResponse = {
|
||||
name: 'Unauthorized',
|
||||
message: 'Token expired',
|
||||
code: 0,
|
||||
status: 401,
|
||||
type: 'yii\\web\\UnauthorizedHttpException',
|
||||
};
|
||||
|
||||
const badTokenReponse = {
|
||||
name: 'Unauthorized',
|
||||
message: 'You are requesting with an invalid credential.',
|
||||
code: 0,
|
||||
status: 401,
|
||||
type: 'yii\\web\\UnauthorizedHttpException',
|
||||
};
|
||||
|
||||
const incorrectTokenReponse = {
|
||||
name: 'Unauthorized',
|
||||
message: 'Incorrect token',
|
||||
code: 0,
|
||||
status: 401,
|
||||
type: 'yii\\web\\UnauthorizedHttpException',
|
||||
};
|
||||
|
||||
let restart;
|
||||
|
||||
beforeEach(() => {
|
||||
getState.returns({
|
||||
accounts: {
|
||||
active: 1,
|
||||
available: [
|
||||
{
|
||||
id: 1,
|
||||
email,
|
||||
token: 'old token',
|
||||
refreshToken,
|
||||
},
|
||||
],
|
||||
},
|
||||
user: {},
|
||||
});
|
||||
|
||||
restart = sinon.stub().named('restart');
|
||||
|
||||
(authentication.requestToken as any).returns(Promise.resolve(validToken));
|
||||
});
|
||||
|
||||
function assertNewTokenRequest() {
|
||||
expect(authentication.requestToken, 'to have a call satisfying', [
|
||||
refreshToken,
|
||||
]);
|
||||
expect(restart, 'was called');
|
||||
expect(dispatch, 'was called');
|
||||
}
|
||||
|
||||
it('should request new token if expired', () =>
|
||||
expect(
|
||||
middleware.catch(expiredResponse, { options: {} }, restart),
|
||||
'to be fulfilled',
|
||||
).then(assertNewTokenRequest));
|
||||
|
||||
it('should request new token if invalid credential', () =>
|
||||
expect(
|
||||
middleware.catch(badTokenReponse, { options: {} }, restart),
|
||||
'to be fulfilled',
|
||||
).then(assertNewTokenRequest));
|
||||
|
||||
it('should request new token if token is incorrect', () =>
|
||||
expect(
|
||||
middleware.catch(incorrectTokenReponse, { options: {} }, restart),
|
||||
'to be fulfilled',
|
||||
).then(assertNewTokenRequest));
|
||||
|
||||
it('should relogin if no refreshToken', () => {
|
||||
getState.returns({
|
||||
accounts: {
|
||||
active: 1,
|
||||
available: [
|
||||
{
|
||||
id: 1,
|
||||
email,
|
||||
refreshToken: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
auth: {
|
||||
credentials: {},
|
||||
},
|
||||
user: {},
|
||||
});
|
||||
|
||||
return expect(
|
||||
middleware.catch(incorrectTokenReponse, { options: {} }, restart),
|
||||
'to be rejected',
|
||||
).then(() => {
|
||||
assertRelogin();
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass the request through if options.token specified', () => {
|
||||
const promise = middleware.catch(
|
||||
expiredResponse,
|
||||
{
|
||||
options: {
|
||||
token: 'foo',
|
||||
},
|
||||
},
|
||||
restart,
|
||||
);
|
||||
|
||||
return expect(promise, 'to be rejected with', expiredResponse).then(
|
||||
() => {
|
||||
expect(restart, 'was not called');
|
||||
expect(authentication.requestToken, 'was not called');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass the rest of failed requests through', () => {
|
||||
const resp = {};
|
||||
|
||||
const promise = middleware.catch(
|
||||
resp,
|
||||
{
|
||||
options: {},
|
||||
},
|
||||
restart,
|
||||
);
|
||||
|
||||
return expect(promise, 'to be rejected with', resp).then(() => {
|
||||
expect(restart, 'was not called');
|
||||
expect(authentication.requestToken, 'was not called');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
ensureToken,
|
||||
recoverFromTokenError,
|
||||
} from 'app/components/accounts/actions';
|
||||
import { getActiveAccount } from 'app/components/accounts/reducer';
|
||||
import { Store } from 'app/reducers';
|
||||
import { Middleware } from 'app/services/request';
|
||||
|
||||
/**
|
||||
* Ensures, that all user's requests have fresh access token
|
||||
*
|
||||
* @param {object} store - redux store
|
||||
* @param {Function} store.getState
|
||||
* @param {Function} store.dispatch
|
||||
*
|
||||
* @returns {object} - request middleware
|
||||
*/
|
||||
export default function refreshTokenMiddleware({
|
||||
dispatch,
|
||||
getState,
|
||||
}: Store): Middleware {
|
||||
return {
|
||||
async before(req) {
|
||||
const activeAccount = getActiveAccount(getState());
|
||||
const disableMiddleware =
|
||||
!!req.options.token || req.options.token === null;
|
||||
|
||||
const isRefreshTokenRequest = req.url.includes('refresh-token');
|
||||
|
||||
if (!activeAccount || disableMiddleware || isRefreshTokenRequest) {
|
||||
return Promise.resolve(req);
|
||||
}
|
||||
|
||||
return dispatch(ensureToken()).then(() => req);
|
||||
},
|
||||
|
||||
async catch(
|
||||
resp: { status: number; message: string },
|
||||
req,
|
||||
restart,
|
||||
): Promise<any> {
|
||||
const disableMiddleware =
|
||||
!!req.options.token || req.options.token === null;
|
||||
|
||||
if (disableMiddleware) {
|
||||
return Promise.reject(resp);
|
||||
}
|
||||
|
||||
return dispatch(recoverFromTokenError(resp)).then(restart);
|
||||
},
|
||||
};
|
||||
}
|
85
packages/app/components/user/reducer.ts
Normal file
85
packages/app/components/user/reducer.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { UPDATE, SET, CHANGE_LANG } from './actions';
|
||||
|
||||
export interface User {
|
||||
id: number | null;
|
||||
uuid: string | null;
|
||||
token: string;
|
||||
username: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
lang: string;
|
||||
isGuest: boolean;
|
||||
isActive: boolean;
|
||||
isOtpEnabled: boolean;
|
||||
passwordChangedAt: number;
|
||||
hasMojangUsernameCollision: boolean;
|
||||
maskedEmail?: string;
|
||||
shouldAcceptRules?: boolean;
|
||||
}
|
||||
|
||||
export type State = {
|
||||
user: User; // TODO: replace with centralized global state
|
||||
};
|
||||
|
||||
const defaults: User = {
|
||||
id: null,
|
||||
uuid: null,
|
||||
username: '',
|
||||
token: '',
|
||||
email: '',
|
||||
// will contain user's email or masked email
|
||||
// (e.g. ex**ple@em*il.c**) depending on what information user have already provided
|
||||
maskedEmail: '',
|
||||
avatar: '',
|
||||
lang: '',
|
||||
isActive: false,
|
||||
isOtpEnabled: false,
|
||||
shouldAcceptRules: false, // whether user need to review updated rules
|
||||
passwordChangedAt: 0,
|
||||
hasMojangUsernameCollision: false,
|
||||
|
||||
// frontend specific attributes
|
||||
isGuest: true,
|
||||
};
|
||||
|
||||
export default function user(
|
||||
state: User = defaults,
|
||||
{ type, payload }: { type: string; payload: any },
|
||||
) {
|
||||
switch (type) {
|
||||
case CHANGE_LANG:
|
||||
if (!payload || !payload.lang) {
|
||||
throw new Error('payload.lang is required for user reducer');
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
lang: payload.lang,
|
||||
};
|
||||
|
||||
case UPDATE:
|
||||
if (!payload) {
|
||||
throw new Error('payload is required for user reducer');
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
...payload,
|
||||
};
|
||||
|
||||
case SET:
|
||||
payload = payload || {};
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
...payload,
|
||||
};
|
||||
|
||||
default:
|
||||
return (
|
||||
state || {
|
||||
...defaults,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user