mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Adds middleware to logout user for 401 errors (#1121)
This commit is contained in:
parent
1b54ce18ab
commit
f655ca5966
3
frontend/app_constants/HTTP_STATUS.js
Normal file
3
frontend/app_constants/HTTP_STATUS.js
Normal file
@ -0,0 +1,3 @@
|
||||
export default {
|
||||
UNAUTHENTICATED: 401,
|
||||
};
|
@ -1,7 +1,9 @@
|
||||
import APP_SETTINGS from 'app_constants/APP_SETTINGS';
|
||||
import HTTP_STATUS from 'app_constants/HTTP_STATUS';
|
||||
import PATHS from 'router/paths';
|
||||
|
||||
export default {
|
||||
APP_SETTINGS,
|
||||
HTTP_STATUS,
|
||||
PATHS,
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { push } from 'react-router-redux';
|
||||
import { join, values } from 'lodash';
|
||||
import { join, omit, values } from 'lodash';
|
||||
|
||||
import queryActions from 'redux/nodes/entities/queries/actions';
|
||||
import { renderFlash } from 'redux/nodes/notifications/actions';
|
||||
@ -7,7 +7,7 @@ import { renderFlash } from 'redux/nodes/notifications/actions';
|
||||
export const fetchQuery = (dispatch, queryID) => {
|
||||
return dispatch(queryActions.load(queryID))
|
||||
.catch((errors) => {
|
||||
const errorMessage = join(values(errors), ', ');
|
||||
const errorMessage = join(values(omit(errors, 'http_status')), ', ');
|
||||
|
||||
dispatch(push('/queries/new'));
|
||||
dispatch(renderFlash('error', errorMessage));
|
||||
|
@ -39,9 +39,10 @@ class Base {
|
||||
}
|
||||
|
||||
const error = new Error(response.statusText);
|
||||
error.response = jsonResponse;
|
||||
error.message = jsonResponse;
|
||||
error.error = jsonResponse.error;
|
||||
error.message = jsonResponse;
|
||||
error.response = jsonResponse;
|
||||
error.status = response.status;
|
||||
|
||||
throw error;
|
||||
});
|
||||
|
@ -1,14 +1,23 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { get } from 'lodash';
|
||||
import { push } from 'react-router-redux';
|
||||
|
||||
import kolide from '../../kolide';
|
||||
import { LOGIN_FAILURE, LOGIN_SUCCESS, LOGOUT_SUCCESS } from '../nodes/auth/actions';
|
||||
import local from '../../utilities/local';
|
||||
import paths from '../../router/paths';
|
||||
import APP_CONSTANTS from 'app_constants';
|
||||
import kolide from 'kolide';
|
||||
import { LOGIN_FAILURE, LOGIN_SUCCESS, LOGOUT_SUCCESS, logoutSuccess } from 'redux/nodes/auth/actions';
|
||||
import local from 'utilities/local';
|
||||
|
||||
const { HTTP_STATUS, PATHS } = APP_CONSTANTS;
|
||||
|
||||
const authMiddleware = store => next => (action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
if (type.endsWith('FAILURE')) {
|
||||
if (get(payload, 'errors.http_status') === HTTP_STATUS.UNAUTHENTICATED) {
|
||||
store.dispatch(logoutSuccess);
|
||||
}
|
||||
}
|
||||
|
||||
if (type === LOGIN_SUCCESS) {
|
||||
const { token } = payload;
|
||||
|
||||
@ -19,7 +28,7 @@ const authMiddleware = store => next => (action) => {
|
||||
}
|
||||
|
||||
if (type === LOGOUT_SUCCESS || type === LOGIN_FAILURE) {
|
||||
const { LOGIN } = paths;
|
||||
const { LOGIN } = PATHS;
|
||||
|
||||
local.clear();
|
||||
kolide.setBearerToken(null);
|
||||
|
@ -65,6 +65,7 @@ describe('Auth - actions', () => {
|
||||
},
|
||||
];
|
||||
const errorResponse = {
|
||||
status: 422,
|
||||
message: {
|
||||
message: 'Unable to perform reset',
|
||||
errors,
|
||||
@ -99,7 +100,7 @@ describe('Auth - actions', () => {
|
||||
{ type: PERFORM_REQUIRED_PASSWORD_RESET_REQUEST },
|
||||
{
|
||||
type: PERFORM_REQUIRED_PASSWORD_RESET_FAILURE,
|
||||
payload: { errors: { base: 'Unable to reset password' } },
|
||||
payload: { errors: { base: 'Unable to reset password', http_status: 422 } },
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -99,7 +99,7 @@ describe('ForgotPasswordPage - reducer', () => {
|
||||
.catch(() => {
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toInclude(forgotPasswordErrorAction({ base: 'Something went wrong' }));
|
||||
expect(actions).toInclude(forgotPasswordErrorAction({ base: 'Something went wrong', http_status: 422 }));
|
||||
expect(invalidRequest.isDone()).toEqual(true);
|
||||
done();
|
||||
});
|
||||
|
@ -38,6 +38,7 @@ describe('ManageHostsPage - reducer', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches the correct actions when successful', (done) => {
|
||||
const statusLabelCounts = { online_count: 23, offline_count: 100, mia_count: 2 };
|
||||
const store = { components: { ManageHostsPage: initialState } };
|
||||
@ -65,12 +66,12 @@ describe('ManageHostsPage - reducer', () => {
|
||||
const store = { components: { ManageHostsPage: initialState } };
|
||||
const mockStore = reduxMockStore(store);
|
||||
const errors = [{ name: 'error_name', reason: 'error reason' }];
|
||||
const errorObject = { message: { message: 'oops', errors } };
|
||||
const errorObject = { status: 422, message: { message: 'oops', errors } };
|
||||
const expectedActions = [
|
||||
{ type: 'LOAD_STATUS_LABEL_COUNTS' },
|
||||
{
|
||||
type: 'GET_STATUS_LABEL_COUNTS_FAILURE',
|
||||
payload: { errors: { error_name: 'error reason' } },
|
||||
payload: { errors: { error_name: 'error reason', http_status: 422 } },
|
||||
},
|
||||
];
|
||||
|
||||
@ -144,11 +145,11 @@ describe('ManageHostsPage - reducer', () => {
|
||||
const store = { components: { ManageHostsPage: initialState } };
|
||||
const mockStore = reduxMockStore(store);
|
||||
const errors = [{ name: 'error_name', reason: 'error reason' }];
|
||||
const errorObject = { message: { message: 'oops', errors } };
|
||||
const errorObject = { status: 422, message: { message: 'oops', errors } };
|
||||
const expectedActions = [
|
||||
{
|
||||
type: 'GET_STATUS_LABEL_COUNTS_FAILURE',
|
||||
payload: { errors: { error_name: 'error reason' } },
|
||||
payload: { errors: { error_name: 'error reason', http_status: 422 } },
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -29,7 +29,10 @@ const formatServerErrors = (errors) => {
|
||||
export const formatErrorResponse = (errorResponse) => {
|
||||
const errors = get(errorResponse, 'message.errors') || [];
|
||||
|
||||
return formatServerErrors(errors);
|
||||
return {
|
||||
...formatServerErrors(errors),
|
||||
http_status: errorResponse.status,
|
||||
};
|
||||
};
|
||||
|
||||
export default { entitiesExceptID, formatErrorResponse };
|
||||
|
@ -40,6 +40,7 @@ describe('reduxConfig - helpers', () => {
|
||||
},
|
||||
];
|
||||
const errorResponse = {
|
||||
status: 422,
|
||||
message: {
|
||||
message: 'Validation Failed',
|
||||
errors,
|
||||
@ -48,6 +49,7 @@ describe('reduxConfig - helpers', () => {
|
||||
|
||||
expect(formatErrorResponse(errorResponse)).toEqual({
|
||||
first_name: 'is not valid, must be something else',
|
||||
http_status: 422,
|
||||
last_name: 'must be changed or something',
|
||||
});
|
||||
});
|
||||
|
@ -22,6 +22,13 @@ const store = {
|
||||
};
|
||||
const invite = { id: 1, name: 'Gnar Dog', email: 'hi@thegnar.co' };
|
||||
const user = { id: 1, email: 'hi@thegnar.co' };
|
||||
const unauthenticatedError = {
|
||||
status: 401,
|
||||
message: {
|
||||
message: 'Unauthenticated',
|
||||
errors: [{ base: 'User is not authenticated' }],
|
||||
},
|
||||
};
|
||||
|
||||
describe('reduxConfig', () => {
|
||||
afterEach(restoreSpies);
|
||||
@ -102,6 +109,28 @@ describe('reduxConfig', () => {
|
||||
|
||||
describe('unsuccessful create call', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
describe('unauthenticated error', () => {
|
||||
const createFunc = createSpy().andCall(() => Promise.reject(unauthenticatedError));
|
||||
const config = reduxConfig({
|
||||
createFunc,
|
||||
entityName: 'users',
|
||||
schema: schemas.USERS,
|
||||
});
|
||||
const { actions } = config;
|
||||
|
||||
it('dispatches the LOGOUT_SUCCESS action', (done) => {
|
||||
mockStore.dispatch(actions.create())
|
||||
.then(done)
|
||||
.catch(() => {
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
|
||||
expect(dispatchedActions).toInclude({ type: 'LOGOUT_SUCCESS' });
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const errors = [
|
||||
{ name: 'first_name',
|
||||
reason: 'is not valid',
|
||||
@ -222,69 +251,92 @@ describe('reduxConfig', () => {
|
||||
});
|
||||
|
||||
describe('unsuccessful update call', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
const errors = [
|
||||
{ name: 'first_name',
|
||||
reason: 'is not valid',
|
||||
},
|
||||
{ name: 'last_name',
|
||||
reason: 'must be changed or something',
|
||||
},
|
||||
];
|
||||
const errorResponse = {
|
||||
message: {
|
||||
message: 'Validation Failed',
|
||||
errors,
|
||||
},
|
||||
};
|
||||
const formattedErrors = formatErrorResponse(errorResponse);
|
||||
const updateFunc = createSpy().andCall(() => {
|
||||
return Promise.reject(errorResponse);
|
||||
});
|
||||
const config = reduxConfig({
|
||||
entityName: 'users',
|
||||
schema: schemas.USERS,
|
||||
updateFunc,
|
||||
});
|
||||
const { actions, reducer } = config;
|
||||
describe('unauthenticated error', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
const updateFunc = createSpy().andCall(() => Promise.reject(unauthenticatedError));
|
||||
const config = reduxConfig({ updateFunc, entityName: 'users', schema: schemas.USERS });
|
||||
const { actions } = config;
|
||||
|
||||
it('calls the updateFunc', () => {
|
||||
mockStore.dispatch(actions.update(user));
|
||||
it('dispatches the LOGOUT_SUCCESS action', (done) => {
|
||||
mockStore.dispatch(actions.update())
|
||||
.then(done)
|
||||
.catch(() => {
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
|
||||
expect(updateFunc).toHaveBeenCalledWith(user);
|
||||
});
|
||||
expect(dispatchedActions).toInclude({ type: 'LOGOUT_SUCCESS' });
|
||||
|
||||
it('dispatches the correct actions', () => {
|
||||
mockStore.dispatch(actions.update());
|
||||
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
const dispatchedActionTypes = dispatchedActions.map((action) => { return action.type; });
|
||||
|
||||
expect(dispatchedActionTypes).toInclude('users_UPDATE_REQUEST');
|
||||
expect(dispatchedActionTypes).toNotInclude('users_UPDATE_SUCCESS');
|
||||
|
||||
const updateFailureAction = find(dispatchedActions, { type: 'users_UPDATE_FAILURE' });
|
||||
|
||||
expect(updateFailureAction.payload).toEqual({
|
||||
errors: formattedErrors,
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('adds the returned errors to state', () => {
|
||||
const updateFailureAction = {
|
||||
type: 'users_UPDATE_FAILURE',
|
||||
payload: {
|
||||
errors: formattedErrors,
|
||||
describe('unprocessable entitiy', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
const errors = [
|
||||
{ name: 'first_name',
|
||||
reason: 'is not valid',
|
||||
},
|
||||
{ name: 'last_name',
|
||||
reason: 'must be changed or something',
|
||||
},
|
||||
];
|
||||
const errorResponse = {
|
||||
status: 422,
|
||||
message: {
|
||||
message: 'Validation Failed',
|
||||
errors,
|
||||
},
|
||||
};
|
||||
const initialState = {
|
||||
loading: false,
|
||||
entities: {},
|
||||
errors: {},
|
||||
};
|
||||
const newState = reducer(initialState, updateFailureAction);
|
||||
const formattedErrors = formatErrorResponse(errorResponse);
|
||||
const updateFunc = createSpy().andCall(() => {
|
||||
return Promise.reject(errorResponse);
|
||||
});
|
||||
const config = reduxConfig({
|
||||
entityName: 'users',
|
||||
schema: schemas.USERS,
|
||||
updateFunc,
|
||||
});
|
||||
const { actions, reducer } = config;
|
||||
|
||||
expect(newState.errors).toEqual(formattedErrors);
|
||||
it('calls the updateFunc', () => {
|
||||
mockStore.dispatch(actions.update(user));
|
||||
|
||||
expect(updateFunc).toHaveBeenCalledWith(user);
|
||||
});
|
||||
|
||||
it('dispatches the correct actions', () => {
|
||||
mockStore.dispatch(actions.update());
|
||||
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
const dispatchedActionTypes = dispatchedActions.map((action) => { return action.type; });
|
||||
|
||||
expect(dispatchedActionTypes).toInclude('users_UPDATE_REQUEST');
|
||||
expect(dispatchedActionTypes).toNotInclude('users_UPDATE_SUCCESS');
|
||||
|
||||
const updateFailureAction = find(dispatchedActions, { type: 'users_UPDATE_FAILURE' });
|
||||
|
||||
expect(updateFailureAction.payload).toEqual({
|
||||
errors: formattedErrors,
|
||||
});
|
||||
});
|
||||
|
||||
it('adds the returned errors to state', () => {
|
||||
const updateFailureAction = {
|
||||
type: 'users_UPDATE_FAILURE',
|
||||
payload: {
|
||||
errors: formattedErrors,
|
||||
},
|
||||
};
|
||||
const initialState = {
|
||||
loading: false,
|
||||
entities: {},
|
||||
errors: {},
|
||||
};
|
||||
const newState = reducer(initialState, updateFailureAction);
|
||||
|
||||
expect(newState.errors).toEqual(formattedErrors);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -70,6 +70,7 @@ describe('Users - actions', () => {
|
||||
},
|
||||
];
|
||||
const errorResponse = {
|
||||
status: 422,
|
||||
message: {
|
||||
message: 'Unable to enable the user',
|
||||
errors,
|
||||
@ -105,7 +106,7 @@ describe('Users - actions', () => {
|
||||
|
||||
expect(dispatchedActions).toEqual([
|
||||
config.extendedActions.updateRequest,
|
||||
config.extendedActions.updateFailure({ base: 'Unable to enable the user' }),
|
||||
config.extendedActions.updateFailure({ base: 'Unable to enable the user', http_status: 422 }),
|
||||
]);
|
||||
|
||||
done();
|
||||
@ -168,6 +169,7 @@ describe('Users - actions', () => {
|
||||
},
|
||||
];
|
||||
const errorResponse = {
|
||||
status: 422,
|
||||
message: {
|
||||
message: 'Unable to change password',
|
||||
errors,
|
||||
@ -203,7 +205,7 @@ describe('Users - actions', () => {
|
||||
|
||||
expect(dispatchedActions).toEqual([
|
||||
config.extendedActions.updateRequest,
|
||||
config.extendedActions.updateFailure({ base: 'Unable to change password' }),
|
||||
config.extendedActions.updateFailure({ base: 'Unable to change password', http_status: 422 }),
|
||||
]);
|
||||
|
||||
done();
|
||||
@ -263,6 +265,7 @@ describe('Users - actions', () => {
|
||||
},
|
||||
];
|
||||
const errorResponse = {
|
||||
status: 422,
|
||||
message: {
|
||||
message: 'Unable to make the user an admin',
|
||||
errors,
|
||||
@ -298,7 +301,7 @@ describe('Users - actions', () => {
|
||||
|
||||
expect(dispatchedActions).toEqual([
|
||||
config.extendedActions.updateRequest,
|
||||
config.extendedActions.updateFailure({ base: 'Unable to make the user an admin' }),
|
||||
config.extendedActions.updateFailure({ base: 'Unable to make the user an admin', http_status: 422 }),
|
||||
]);
|
||||
|
||||
done();
|
||||
@ -352,6 +355,7 @@ describe('Users - actions', () => {
|
||||
},
|
||||
];
|
||||
const errorResponse = {
|
||||
status: 422,
|
||||
message: {
|
||||
message: 'Unable to require password reset',
|
||||
errors,
|
||||
@ -385,7 +389,7 @@ describe('Users - actions', () => {
|
||||
{ type: REQUIRE_PASSWORD_RESET_REQUEST },
|
||||
{
|
||||
type: REQUIRE_PASSWORD_RESET_FAILURE,
|
||||
payload: { errors: { base: 'Unable to require password reset' } },
|
||||
payload: { errors: { base: 'Unable to require password reset', http_status: 422 } },
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -5,12 +5,14 @@ import { noop } from 'lodash';
|
||||
import { Provider } from 'react-redux';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import authMiddleware from 'redux/middlewares/auth';
|
||||
|
||||
export const fillInFormInput = (inputComponent, value) => {
|
||||
return inputComponent.simulate('change', { target: { value } });
|
||||
};
|
||||
|
||||
export const reduxMockStore = (store = {}) => {
|
||||
const middlewares = [thunk];
|
||||
const middlewares = [thunk, authMiddleware];
|
||||
const mockStore = configureStore(middlewares);
|
||||
|
||||
return mockStore(store);
|
||||
|
Loading…
Reference in New Issue
Block a user