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"