From 524e8bffbfaf125bbdc8632f49dc8a290a70eaba Mon Sep 17 00:00:00 2001 From: zmagod Date: Thu, 5 Oct 2017 10:07:50 +0200 Subject: [PATCH 1/3] fixes sign out action on settings page, clean the redux store, handles redirect to sign in page if session is terminated [fixes SCI-1567] --- .babelrc | 1 + .../client_api/users/users_controller.rb | 10 +++++ .../components/UserAccountDropdown.jsx | 45 ++++++++++--------- .../src/components/actions/UsersActions.js | 5 +++ app/javascript/src/config/action_types.js | 3 +- app/javascript/src/config/axios.js | 12 +++++ app/javascript/src/config/reducers.js | 14 +++++- app/javascript/src/config/routes.js | 4 +- app/javascript/src/services/api/config.js | 21 +++++++++ app/javascript/src/services/api/endpoints.js | 1 + app/javascript/src/services/api/users_api.js | 6 +++ config/routes.rb | 1 + package.json | 5 ++- 13 files changed, 101 insertions(+), 27 deletions(-) create mode 100644 app/javascript/src/services/api/config.js create mode 100644 app/javascript/src/services/api/endpoints.js create mode 100644 app/javascript/src/services/api/users_api.js diff --git a/.babelrc b/.babelrc index 98c364a94..3c71c69e3 100644 --- a/.babelrc +++ b/.babelrc @@ -17,6 +17,7 @@ "plugins": [ "transform-object-rest-spread", "syntax-dynamic-import", + "transform-react-jsx-source", [ "transform-class-properties", { diff --git a/app/controllers/client_api/users/users_controller.rb b/app/controllers/client_api/users/users_controller.rb index 3aa443cf1..e9d03c5fb 100644 --- a/app/controllers/client_api/users/users_controller.rb +++ b/app/controllers/client_api/users/users_controller.rb @@ -2,6 +2,16 @@ module ClientApi module Users class UsersController < ApplicationController + def sign_out_user + respond_to do |format| + if sign_out current_user + format.json { render json: {}, status: :ok } + else + format.json { render json: {}, status: :unauthorized } + end + end + end + def preferences_info respond_to do |format| format.json do diff --git a/app/javascript/src/components/Navigation/components/UserAccountDropdown.jsx b/app/javascript/src/components/Navigation/components/UserAccountDropdown.jsx index caa239fb8..932df3c6b 100644 --- a/app/javascript/src/components/Navigation/components/UserAccountDropdown.jsx +++ b/app/javascript/src/components/Navigation/components/UserAccountDropdown.jsx @@ -1,22 +1,33 @@ import React, { Component } from "react"; import { connect } from "react-redux"; -import PropTypes from "prop-types"; +import { func, shape, string, number } from "prop-types"; import { NavDropdown, MenuItem, Image } from "react-bootstrap"; import styled from "styled-components"; import { FormattedMessage } from "react-intl"; +import { SIGN_IN_PATH } from "../../../config/routes"; -import { getCurrentUser } from "../../actions/UsersActions"; +import { getCurrentUser, destroyState } from "../../actions/UsersActions"; +import { signOutUser } from "../../../services/api/users_api"; const StyledNavDropdown = styled(NavDropdown)` -& #user-account-dropdown { - padding-top: 10px; - padding-bottom: 10px; -} + & #user-account-dropdown { + padding-top: 10px; + padding-bottom: 10px; + } `; class UserAccountDropdown extends Component { componentDidMount() { this.props.getCurrentUser(); + this.signOut = this.signOut.bind(this); + } + + signOut() { + document.querySelector('meta[name="csrf-token"]').remove(); + signOutUser().then(() => { + this.props.destroyState(); + window.location = SIGN_IN_PATH; + }); } render() { @@ -43,7 +54,7 @@ class UserAccountDropdown extends Component { - + @@ -52,24 +63,18 @@ class UserAccountDropdown extends Component { } UserAccountDropdown.propTypes = { - getCurrentUser: PropTypes.func.isRequired, - current_user: PropTypes.shape({ - id: PropTypes.number.isRequired, - fullName: PropTypes.string.isRequired, - avatarPath: PropTypes.string.isRequired + getCurrentUser: func.isRequired, + destroyState: func.isRequired, + current_user: shape({ + id: number.isRequired, + fullName: string.isRequired, + avatarPath: string.isRequired }).isRequired }; // Map the states from store to component const mapStateToProps = ({ current_user }) => ({ current_user }); -// Map the fetch activity action to component -const mapDispatchToProps = dispatch => ({ - getCurrentUser() { - dispatch(getCurrentUser()); - } -}); - -export default connect(mapStateToProps, mapDispatchToProps)( +export default connect(mapStateToProps, { destroyState, getCurrentUser })( UserAccountDropdown ); diff --git a/app/javascript/src/components/actions/UsersActions.js b/app/javascript/src/components/actions/UsersActions.js index c6a25deba..0cc03c7fc 100644 --- a/app/javascript/src/components/actions/UsersActions.js +++ b/app/javascript/src/components/actions/UsersActions.js @@ -15,6 +15,7 @@ import { } from "../../config/api_endpoints"; import { + USER_LOGOUT, SET_CURRENT_USER, CHANGE_CURRENT_USER_FULL_NAME, CHANGE_CURRENT_USER_INITIALS, @@ -29,6 +30,10 @@ import { CHANGE_SYSTEM_MESSAGE_NOTIFICATION_EMAIL } from "../../config/action_types"; +export function destroyState() { + return { type: USER_LOGOUT }; +} + function addCurrentUser(data) { return { type: SET_CURRENT_USER, diff --git a/app/javascript/src/config/action_types.js b/app/javascript/src/config/action_types.js index 732a81db4..1108bf936 100644 --- a/app/javascript/src/config/action_types.js +++ b/app/javascript/src/config/action_types.js @@ -8,6 +8,7 @@ export const GLOBAL_ACTIVITIES_DATA = "GLOBAL_ACTIVITIES_DATA"; export const DESTROY_GLOBAL_ACTIVITIES_DATA = "DESTROY_GLOBAL_ACTIVITIES_DATA"; // users +export const USER_LOGOUT = "USER_LOGOUT"; export const SET_CURRENT_USER = "SET_CURRENT_USER"; export const CHANGE_CURRENT_USER_FULL_NAME = "CHANGE_CURRENT_USER_FULL_NAME"; export const CHANGE_CURRENT_USER_INITIALS = "CHANGE_CURRENT_USER_INITIALS"; @@ -39,4 +40,4 @@ export const SPINNER_OFF = "SPINNER_OFF"; // alerts export const ADD_ALERT = "ADD_ALERT"; export const CLEAR_ALERT = "CLEAR_ALERT"; -export const CLEAR_ALL_ALERTS = "CLEAR_ALL_ALERTS"; \ No newline at end of file +export const CLEAR_ALL_ALERTS = "CLEAR_ALL_ALERTS"; diff --git a/app/javascript/src/config/axios.js b/app/javascript/src/config/axios.js index 7cf8426c7..b514b7001 100644 --- a/app/javascript/src/config/axios.js +++ b/app/javascript/src/config/axios.js @@ -1,7 +1,19 @@ +// @TODO remove this file ASAP the preferences/profile refactoring is merged import axios from "axios"; +import store from "./store"; +import { SIGN_IN_PATH } from "./routes"; +import { destroyState } from "../components/actions/UsersActions"; export default axios.create({ + withCredentials: true, headers: { "X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content + }, + validateStatus(status) { + if (status === 401) { + store.dispatch(destroyState); + window.location = SIGN_IN_PATH; + } + return status >= 200 && status < 300; } }); diff --git a/app/javascript/src/config/reducers.js b/app/javascript/src/config/reducers.js index 5561d4cb3..ead7707fd 100644 --- a/app/javascript/src/config/reducers.js +++ b/app/javascript/src/config/reducers.js @@ -1,14 +1,15 @@ import { combineReducers } from "redux"; +import { USER_LOGOUT } from "./action_types"; import { setCurrentTeam, getListOfTeams, - showLeaveTeamModal, + showLeaveTeamModal } from "../components/reducers/TeamReducers"; import { globalActivities } from "../components/reducers/ActivitiesReducers"; import { currentUser } from "../components/reducers/UsersReducer"; import { alerts } from "../components/reducers/AlertsReducers"; -export default combineReducers({ +const appReducer = combineReducers({ current_team: setCurrentTeam, all_teams: getListOfTeams, global_activities: globalActivities, @@ -16,3 +17,12 @@ export default combineReducers({ showLeaveTeamModal, alerts }); + +const rootReducer = (state, action) => { + if (action.type === USER_LOGOUT) { + state = undefined; + } + return appReducer(state, action); +}; + +export default rootReducer; diff --git a/app/javascript/src/config/routes.js b/app/javascript/src/config/routes.js index e25f55853..74abccff0 100644 --- a/app/javascript/src/config/routes.js +++ b/app/javascript/src/config/routes.js @@ -1,9 +1,9 @@ export const ROOT_PATH = "/"; - +export const SIGN_IN_PATH = "/users/sign_in"; // Settings page export const SETTINGS_TEAMS_ROUTE = "/settings/teams"; export const SETTINGS_TEAM_ROUTE = "/settings/teams/:id"; export const SETTINGS_NEW_TEAM_ROUTE = "/settings/teams/new"; export const SETTINGS_ACCOUNT_PROFILE = "/settings/account/profile"; -export const SETTINGS_ACCOUNT_PREFERENCES = "/settings/account/preferences"; \ No newline at end of file +export const SETTINGS_ACCOUNT_PREFERENCES = "/settings/account/preferences"; diff --git a/app/javascript/src/services/api/config.js b/app/javascript/src/services/api/config.js new file mode 100644 index 000000000..942739121 --- /dev/null +++ b/app/javascript/src/services/api/config.js @@ -0,0 +1,21 @@ +import axios from "axios"; +// import { dispatch } from "redux"; +import store from "../../config/store"; +import { SIGN_IN_PATH } from "../../config/routes"; +import { destroyState } from "../../components/actions/UsersActions"; + +export const axiosInstance = axios.create({ + withCredentials: true, + headers: { + "X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').content + }, + validateStatus(status) { + if (status === 401) { + setTimeout(() => { + store.dispatch(destroyState) + window.location = SIGN_IN_PATH; + }, 500); + } + return status >= 200 && status < 300; + } +}); diff --git a/app/javascript/src/services/api/endpoints.js b/app/javascript/src/services/api/endpoints.js new file mode 100644 index 000000000..c53043eec --- /dev/null +++ b/app/javascript/src/services/api/endpoints.js @@ -0,0 +1 @@ +export const SIGN_OUT_PATH = "/client_api/users/sign_out_user" diff --git a/app/javascript/src/services/api/users_api.js b/app/javascript/src/services/api/users_api.js new file mode 100644 index 000000000..87e543e29 --- /dev/null +++ b/app/javascript/src/services/api/users_api.js @@ -0,0 +1,6 @@ +import { axiosInstance } from "./config"; +import { SIGN_OUT_PATH } from "./endpoints"; + +export function signOutUser() { + return axiosInstance.get(SIGN_OUT_PATH); +} diff --git a/config/routes.rb b/config/routes.rb index 81f0d7304..eabfa0808 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -32,6 +32,7 @@ Rails.application.routes.draw do get '/current_user_info', to: 'users/users#current_user_info' namespace :users do + get '/sign_out_user', to: 'users#sign_out_user' delete '/remove_user', to: 'user_teams#remove_user' delete '/leave_team', to: 'user_teams#leave_team' put '/update_role', to: 'user_teams#update_role' diff --git a/package.json b/package.json index 651b0ceb2..78bf30f87 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "devDependencies": { "babel-eslint": "^7.2.3", + "babel-plugin-transform-react-jsx-source": "^6.22.0", "eslint": "^3.7.1", "eslint-config-airbnb": "^15.1.0", "eslint-config-google": "^0.5.0", @@ -68,8 +69,6 @@ "react-bootstrap-table": "^4.0.0", "react-bootstrap-timezone-picker": "^1.0.11", "react-data-grid": "^2.0.2", - "react-tagsinput": "^3.17.0", - "react-transition-group": "^2.2.0", "react-dom": "^15.6.1", "react-intl": "^2.3.0", "react-intl-redux": "^0.6.0", @@ -78,7 +77,9 @@ "react-router-bootstrap": "^0.24.2", "react-router-dom": "^4.1.2", "react-router-prop-types": "^0.0.1", + "react-tagsinput": "^3.17.0", "react-timezone": "^0.2.0", + "react-transition-group": "^2.2.0", "redux": "^3.7.2", "redux-thunk": "^2.2.0", "resolve-url-loader": "^2.1.0", From 1b03222260bd93fb41f7a48ff3b01cf906a73dc7 Mon Sep 17 00:00:00 2001 From: zmagod Date: Thu, 5 Oct 2017 10:24:28 +0200 Subject: [PATCH 2/3] adds tests for sign_out_user action on users controller --- .../client_api/users/users_controller_spec.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/spec/controllers/client_api/users/users_controller_spec.rb b/spec/controllers/client_api/users/users_controller_spec.rb index c23d17658..5a41ee6ce 100644 --- a/spec/controllers/client_api/users/users_controller_spec.rb +++ b/spec/controllers/client_api/users/users_controller_spec.rb @@ -7,6 +7,20 @@ describe ClientApi::Users::UsersController, type: :controller do @user = User.first end + describe '#sign_out_user' do + it 'returns unauthorized response' do + sign_out @user + get :sign_out_user, format: :json + expect(response).to have_http_status(:unauthorized) + end + + it 'responds successfully if the user is signed out' do + get :sign_out_user, format: :json + expect(response).to have_http_status(:ok) + expect(subject.current_user).to eq(nil) + end + end + describe 'GET current_user_info' do it 'responds successfully' do get :current_user_info, format: :json From 0a8ef8cd9dbe6afe0e68410360678ccd1280d4ed Mon Sep 17 00:00:00 2001 From: zmagod Date: Mon, 9 Oct 2017 09:23:25 +0200 Subject: [PATCH 3/3] removed unneeded import --- app/javascript/src/components/actions/UsersActions.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/javascript/src/components/actions/UsersActions.js b/app/javascript/src/components/actions/UsersActions.js index e927961a6..7bb848ec3 100644 --- a/app/javascript/src/components/actions/UsersActions.js +++ b/app/javascript/src/components/actions/UsersActions.js @@ -1,5 +1,4 @@ import { USER_LOGOUT, SET_CURRENT_USER } from "../../config/action_types"; -import { getCurrentUser } from "../../services/api/users_api"; export function destroyState() { return { type: USER_LOGOUT };