diff --git a/.circleci/config.yml b/.circleci/config.yml index d46ec56bd..ce22c7c86 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -95,24 +95,24 @@ jobs: command: yarn run eslint # Run rails tests - - run: + - run: name: Run backend tests command: | bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) ./tmp/cc-test-reporter format-coverage -t simplecov -o tmp/codeclimate.backend.json coverage/backend/.resultset.json - persist_to_workspace: root: tmp - paths: + paths: - codeclimate.backend.json - - run: + - run: name: Run frontend tests command: | yarn test:coverage ./tmp/cc-test-reporter format-coverage -t lcov -o tmp/codeclimate.frontend.json buildreports/lcov.info - persist_to_workspace: root: tmp - paths: + paths: - codeclimate.frontend.json # collect reports @@ -126,4 +126,4 @@ jobs: name: Upload coverage results to Code Climate command: | ./tmp/cc-test-reporter sum-coverage tmp/codeclimate.*.json -p 2 -o tmp/codeclimate.total.json - ./tmp/cc-test-reporter upload-coverage -i tmp/codeclimate.total.json \ No newline at end of file + ./tmp/cc-test-reporter upload-coverage -i tmp/codeclimate.total.json diff --git a/__mocks__/fileMock.js b/__mocks__/fileMock.js new file mode 100644 index 000000000..9dc5fc1e4 --- /dev/null +++ b/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = ''; diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index 72e432f74..9a0bbfc17 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -1,5 +1,5 @@ class Api::V1::ProfilesController < Api::BaseController - before_action :fetch_user + before_action :set_user def show render json: @user @@ -7,12 +7,11 @@ class Api::V1::ProfilesController < Api::BaseController def update @user.update!(profile_params) - render json: @user end private - def fetch_user + def set_user @user = current_user end diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue index 692b92a65..acf177d7c 100644 --- a/app/javascript/dashboard/App.vue +++ b/app/javascript/dashboard/App.vue @@ -18,8 +18,7 @@ export default { }, mounted() { - this.$store.dispatch('set_user'); - this.$store.dispatch('validityCheck'); + this.$store.dispatch('setUser'); }, }; diff --git a/app/javascript/dashboard/api/auth.js b/app/javascript/dashboard/api/auth.js index 031d02b14..f247089cd 100644 --- a/app/javascript/dashboard/api/auth.js +++ b/app/javascript/dashboard/api/auth.js @@ -1,29 +1,10 @@ /* eslint no-console: 0 */ /* global axios */ /* eslint no-undef: "error" */ -/* eslint-env browser */ -/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ -import moment from 'moment'; import Cookies from 'js-cookie'; import endPoints from './endPoints'; -import { frontendURL } from '../helper/URLHelper'; - -const setAuthCredentials = response => { - const expiryDate = moment.unix(response.headers.expiry); - Cookies.set('auth_data', response.headers, { - expires: expiryDate.diff(moment(), 'days'), - }); - Cookies.set('user', response.data.data, { - expires: expiryDate.diff(moment(), 'days'), - }); -}; - -const clearCookiesOnLogout = () => { - Cookies.remove('auth_data'); - Cookies.remove('user'); - window.location = frontendURL('login'); -}; +import { setAuthCredentials, clearCookiesOnLogout } from '../store/utils/api'; export default { login(creds) { @@ -60,20 +41,7 @@ export default { }, validityCheck() { const urlData = endPoints('validityCheck'); - const fetchPromise = new Promise((resolve, reject) => { - axios - .get(urlData.url) - .then(response => { - resolve(response); - }) - .catch(error => { - if (error.response.status === 401) { - clearCookiesOnLogout(); - } - reject(error); - }); - }); - return fetchPromise; + return axios.get(urlData.url); }, logout() { const urlData = endPoints('logout'); @@ -136,13 +104,7 @@ export default { password, }) .then(response => { - const expiryDate = moment.unix(response.headers.expiry); - Cookies.set('auth_data', response.headers, { - expires: expiryDate.diff(moment(), 'days'), - }); - Cookies.set('user', response.data.data, { - expires: expiryDate.diff(moment(), 'days'), - }); + setAuthCredentials(response); resolve(response); }) .catch(error => { @@ -155,4 +117,22 @@ export default { const urlData = endPoints('resetPassword'); return axios.post(urlData.url, { email }); }, + + profileUpdate({ name, email, password, password_confirmation, avatar }) { + const formData = new FormData(); + if (name) { + formData.append('profile[name]', name); + } + if (email) { + formData.append('profile[email]', email); + } + if (password && password_confirmation) { + formData.append('profile[password]', password); + formData.append('profile[password_confirmation]', password_confirmation); + } + if (avatar) { + formData.append('profile[avatar]', avatar); + } + return axios.put(endPoints('profileUpdate').url, formData); + }, }; diff --git a/app/javascript/dashboard/api/endPoints.js b/app/javascript/dashboard/api/endPoints.js index 2898362cc..10a0608bd 100644 --- a/app/javascript/dashboard/api/endPoints.js +++ b/app/javascript/dashboard/api/endPoints.js @@ -10,6 +10,9 @@ const endPoints = { validityCheck: { url: '/auth/validate_token', }, + profileUpdate: { + url: '/api/v1/profile', + }, logout: { url: 'auth/sign_out', }, diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue index 6091678c7..e4b9c22f8 100644 --- a/app/javascript/dashboard/components/layout/Sidebar.vue +++ b/app/javascript/dashboard/components/layout/Sidebar.vue @@ -42,12 +42,21 @@ class="dropdown-pane top" >
- +

{{ currentUser.name }} @@ -65,7 +74,6 @@ + + diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js b/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js new file mode 100644 index 000000000..d632363b7 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/profile.routes.js @@ -0,0 +1,27 @@ +import SettingsContent from '../Wrapper'; +import Index from './Index.vue'; +import { frontendURL } from '../../../../helper/URLHelper'; + +export default { + routes: [ + { + path: frontendURL('profile'), + name: 'profile_settings', + roles: ['administrator', 'agent'], + component: SettingsContent, + props: { + headerTitle: 'PROFILE_SETTINGS.TITLE', + icon: 'ion-compose', + showNewButton: false, + }, + children: [ + { + path: 'settings', + name: 'profile_settings_index', + component: Index, + roles: ['administrator', 'agent'], + }, + ], + }, + ], +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js index 38a00427e..bd1af5b01 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/settings.routes.js @@ -1,10 +1,11 @@ -import agent from './agents/agent.routes'; -import inbox from './inbox/inbox.routes'; -import canned from './canned/canned.routes'; -import reports from './reports/reports.routes'; -import billing from './billing/billing.routes'; -import Auth from '../../../api/auth'; import { frontendURL } from '../../../helper/URLHelper'; +import agent from './agents/agent.routes'; +import Auth from '../../../api/auth'; +import billing from './billing/billing.routes'; +import canned from './canned/canned.routes'; +import inbox from './inbox/inbox.routes'; +import profile from './profile/profile.routes'; +import reports from './reports/reports.routes'; export default { routes: [ @@ -19,10 +20,11 @@ export default { return frontendURL('settings/canned-response'); }, }, - ...inbox.routes, ...agent.routes, - ...canned.routes, - ...reports.routes, ...billing.routes, + ...canned.routes, + ...inbox.routes, + ...profile.routes, + ...reports.routes, ], }; diff --git a/app/javascript/dashboard/store/modules/auth.js b/app/javascript/dashboard/store/modules/auth.js index a0d1f692e..0cd94e6c8 100644 --- a/app/javascript/dashboard/store/modules/auth.js +++ b/app/javascript/dashboard/store/modules/auth.js @@ -1,5 +1,3 @@ -/* eslint no-console: 0 */ -/* eslint-env browser */ /* eslint no-param-reassign: 0 */ import axios from 'axios'; import moment from 'moment'; @@ -9,7 +7,8 @@ import router from '../../routes'; import authAPI from '../../api/auth'; import createAxios from '../../helper/APIHelper'; import actionCable from '../../helper/actionCable'; -// initial state +import { setUser, getHeaderExpiry, clearCookiesOnLogout } from '../utils/api'; + const state = { currentUser: { id: null, @@ -27,15 +26,19 @@ const state = { }; // getters -const getters = { - isLoggedIn(_state) { - return _state.currentUser.id !== null; +export const getters = { + isLoggedIn($state) { + return !!$state.currentUser.id; }, getCurrentUserID(_state) { return _state.currentUser.id; }, + getCurrentUser(_state) { + return _state.currentUser; + }, + getSubscription(_state) { return _state.currentUser.subscription === undefined ? null @@ -53,7 +56,7 @@ const getters = { }; // actions -const actions = { +export const actions = { login({ commit }, credentials) { return new Promise((resolve, reject) => { authAPI @@ -70,14 +73,21 @@ const actions = { }); }); }, - validityCheck(context) { - if (context.getters.isLoggedIn) { - authAPI.validityCheck(); + async validityCheck(context) { + try { + const response = await authAPI.validityCheck(); + setUser(response.data.payload.data, getHeaderExpiry(response)); + context.commit(types.default.SET_CURRENT_USER); + } catch (error) { + if (error.response.status === 401) { + clearCookiesOnLogout(); + } } }, - set_user({ commit }) { + setUser({ commit, dispatch }) { if (authAPI.isLoggedIn()) { commit(types.default.SET_CURRENT_USER); + dispatch('validityCheck'); } else { commit(types.default.CLEAR_USER); } @@ -85,6 +95,15 @@ const actions = { logout({ commit }) { commit(types.default.CLEAR_USER); }, + updateProfile: async ({ commit }, params) => { + try { + const response = await authAPI.profileUpdate(params); + setUser(response.data, getHeaderExpiry(response)); + commit(types.default.SET_CURRENT_USER); + } catch (error) { + // Ignore error + } + }, }; // mutations @@ -93,8 +112,12 @@ const mutations = { _state.currentUser.id = null; }, [types.default.SET_CURRENT_USER](_state) { - Object.assign(_state.currentUser, authAPI.getAuthData()); - Object.assign(_state.currentUser, authAPI.getCurrentUser()); + const currentUser = { + ...authAPI.getAuthData(), + ...authAPI.getCurrentUser(), + }; + + Vue.set(_state, 'currentUser', currentUser); }, }; diff --git a/app/javascript/dashboard/store/modules/specs/auth/actions.spec.js b/app/javascript/dashboard/store/modules/specs/auth/actions.spec.js new file mode 100644 index 000000000..287bb1be3 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/auth/actions.spec.js @@ -0,0 +1,70 @@ +import axios from 'axios'; +import Cookies from 'js-cookie'; +import { actions } from '../../auth'; +import * as types from '../../../mutation-types'; +import { setUser, clearCookiesOnLogout } from '../../../utils/api'; +import '../../../../routes'; + +jest.mock('../../../../routes', () => {}); +jest.mock('../../../utils/api', () => ({ + setUser: jest.fn(), + clearCookiesOnLogout: jest.fn(), + getHeaderExpiry: jest.fn(), +})); +jest.mock('js-cookie', () => ({ + getJSON: jest.fn(), +})); + +const commit = jest.fn(); +const dispatch = jest.fn(); +global.axios = axios; +jest.mock('axios'); + +describe('#actions', () => { + describe('#validityCheck', () => { + it('sends correct actions if API is success', async () => { + axios.get.mockResolvedValue({ + data: { payload: { data: { id: 1, name: 'John' } } }, + headers: { expiry: 581842904 }, + }); + await actions.validityCheck({ commit }); + expect(setUser).toHaveBeenCalledTimes(1); + expect(commit.mock.calls).toEqual([[types.default.SET_CURRENT_USER]]); + }); + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ + response: { status: 401 }, + }); + await actions.validityCheck({ commit }); + expect(clearCookiesOnLogout); + }); + }); + + describe('#updateProfile', () => { + it('sends correct actions if API is success', async () => { + axios.put.mockResolvedValue({ + data: { id: 1, name: 'John' }, + headers: { expiry: 581842904 }, + }); + await actions.updateProfile({ commit }, { name: 'Pranav' }); + expect(setUser).toHaveBeenCalledTimes(1); + expect(commit.mock.calls).toEqual([[types.default.SET_CURRENT_USER]]); + }); + }); + + describe('#setUser', () => { + it('sends correct actions if user is logged in', async () => { + Cookies.getJSON.mockImplementation(() => true); + actions.setUser({ commit, dispatch }); + expect(commit.mock.calls).toEqual([[types.default.SET_CURRENT_USER]]); + expect(dispatch.mock.calls).toEqual([['validityCheck']]); + }); + + it('sends correct actions if user is not logged in', async () => { + Cookies.getJSON.mockImplementation(() => false); + actions.setUser({ commit, dispatch }); + expect(commit.mock.calls).toEqual([[types.default.CLEAR_USER]]); + expect(dispatch).toHaveBeenCalledTimes(0); + }); + }); +}); diff --git a/app/javascript/dashboard/store/modules/specs/auth/getters.spec.js b/app/javascript/dashboard/store/modules/specs/auth/getters.spec.js new file mode 100644 index 000000000..d39f5d991 --- /dev/null +++ b/app/javascript/dashboard/store/modules/specs/auth/getters.spec.js @@ -0,0 +1,20 @@ +import { getters } from '../../auth'; + +import '../../../../routes'; + +jest.mock('../../../../routes', () => {}); +describe('#getters', () => { + it('isLoggedIn', () => { + expect(getters.isLoggedIn({ currentUser: { id: null } })).toEqual(false); + expect(getters.isLoggedIn({ currentUser: { id: 1 } })).toEqual(true); + }); + + it('getCurrentUserID', () => { + expect(getters.getCurrentUserID({ currentUser: { id: 1 } })).toEqual(1); + }); + it('getCurrentUser', () => { + expect( + getters.getCurrentUser({ currentUser: { id: 1, name: 'Pranav' } }) + ).toEqual({ id: 1, name: 'Pranav' }); + }); +}); diff --git a/app/javascript/dashboard/store/utils/api.js b/app/javascript/dashboard/store/utils/api.js index 1b221994c..1a8a71a07 100644 --- a/app/javascript/dashboard/store/utils/api.js +++ b/app/javascript/dashboard/store/utils/api.js @@ -1,6 +1,30 @@ /* eslint no-param-reassign: 0 */ +import moment from 'moment'; +import Cookies from 'js-cookie'; +import { frontendURL } from '../../helper/URLHelper'; export const getLoadingStatus = state => state.fetchAPIloadingStatus; export const setLoadingStatus = (state, status) => { state.fetchAPIloadingStatus = status; }; + +export const setUser = (userData, expiryDate) => + Cookies.set('user', userData, { + expires: expiryDate.diff(moment(), 'days'), + }); + +export const getHeaderExpiry = response => moment.unix(response.headers.expiry); + +export const setAuthCredentials = response => { + const expiryDate = getHeaderExpiry(response); + Cookies.set('auth_data', response.headers, { + expires: expiryDate.diff(moment(), 'days'), + }); + setUser(response.data.data, expiryDate); +}; + +export const clearCookiesOnLogout = () => { + Cookies.remove('auth_data'); + Cookies.remove('user'); + window.location = frontendURL('login'); +}; diff --git a/app/views/api/v1/profiles/update.json.jbuilder b/app/views/api/v1/profiles/update.json.jbuilder new file mode 100644 index 000000000..b55f96967 --- /dev/null +++ b/app/views/api/v1/profiles/update.json.jbuilder @@ -0,0 +1,11 @@ +json.id @user.id +json.provider @user.provider +json.uid @user.uid +json.name @user.name +json.nickname @user.nickname +json.email @user.email +json.account_id @user.account_id +json.pubsub_token @user.pubsub_token +json.role @user.role +json.confirmed @user.confirmed? +json.avatar_url @user.avatar_url diff --git a/jest.config.js b/jest.config.js index 1986065f4..620fcb82a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -21,6 +21,8 @@ module.exports = { transformIgnorePatterns: ['node_modules/*'], moduleNameMapper: { '^@/(.*)$': '/app/javascript/$1', + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/__mocks__/fileMock.js', }, roots: ['/app/javascript'], snapshotSerializers: ['jest-serializer-vue'], diff --git a/package.json b/package.json index e15ccb217..cf7e2f938 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "ionicons": "~2.0.1", "js-cookie": "^2.2.1", "lodash.groupby": "^4.6.0", - "md5": "~2.2.1", "moment": "~2.19.3", "query-string": "5", "spinkit": "~1.2.5", diff --git a/yarn.lock b/yarn.lock index 4aa7494d0..57a5df811 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2375,11 +2375,6 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -charenc@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" - integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= - chart.js@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.5.0.tgz#fe6e751a893769f56e72bee5ad91207e1c592957" @@ -2857,11 +2852,6 @@ cross-spawn@^3.0.0: lru-cache "^4.0.1" which "^1.2.9" -crypt@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" - integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= - crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -5131,7 +5121,7 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" -is-buffer@^1.1.5, is-buffer@~1.1.1: +is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -6391,15 +6381,6 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" -md5@~2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" - integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= - dependencies: - charenc "~0.0.1" - crypt "~0.0.1" - is-buffer "~1.1.1" - mdn-data@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"