Feature: Agent Profile Update with avatar (#449)

* Feature: Agent Profile Update with avatar
* Add Update Profile with name, avatar, email and password
This commit is contained in:
Pranav Raj S 2020-02-16 17:20:38 +05:30 committed by GitHub
parent e61ba95cf7
commit c4e2a84f65
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 584 additions and 133 deletions

View file

@ -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
./tmp/cc-test-reporter upload-coverage -i tmp/codeclimate.total.json

1
__mocks__/fileMock.js Normal file
View file

@ -0,0 +1 @@
module.exports = '';

View file

@ -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

View file

@ -18,8 +18,7 @@ export default {
},
mounted() {
this.$store.dispatch('set_user');
this.$store.dispatch('validityCheck');
this.$store.dispatch('setUser');
},
};
</script>

View file

@ -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);
},
};

View file

@ -10,6 +10,9 @@ const endPoints = {
validityCheck: {
url: '/auth/validate_token',
},
profileUpdate: {
url: '/api/v1/profile',
},
logout: {
url: 'auth/sign_out',
},

View file

@ -42,12 +42,21 @@
class="dropdown-pane top"
>
<ul class="vertical dropdown menu">
<li><a href="#" @click.prevent="logout()">Logout</a></li>
<li>
<router-link to="/app/profile/settings">
{{ $t('SIDEBAR.PROFILE_SETTINGS') }}
</router-link>
</li>
<li>
<a href="#" @click.prevent="logout()">
{{ $t('SIDEBAR.LOGOUT') }}
</a>
</li>
</ul>
</div>
</transition>
<div class="current-user" @click.prevent="showOptions()">
<thumbnail :src="gravatarUrl()" :username="currentUser.name" />
<thumbnail :src="currentUser.avatar_url" :username="currentUser.name" />
<div class="current-user--data">
<h3 class="current-user--name">
{{ currentUser.name }}
@ -65,7 +74,6 @@
<script>
import { mapGetters } from 'vuex';
import md5 from 'md5';
import { mixin as clickaway } from 'vue-clickaway';
import adminMixin from '../../mixins/isAdmin';
@ -99,6 +107,7 @@ export default {
daysLeft: 'getTrialLeft',
subscriptionData: 'getSubscription',
inboxes: 'inboxes/getInboxes',
currentUser: 'getCurrentUser',
}),
accessibleMenuItems() {
// get all keys in menuGroup
@ -144,9 +153,6 @@ export default {
})),
};
},
currentUser() {
return Auth.getCurrentUser();
},
dashboardPath() {
return frontendURL('dashboard');
},
@ -174,10 +180,6 @@ export default {
this.$store.dispatch('inboxes/get');
},
methods: {
gravatarUrl() {
const hash = md5(this.currentUser.email);
return `${window.WootConstants.GRAVATAR_URL}${hash}?default=404`;
},
filterBillingRoutes(menuItems) {
return menuItems.filter(
menuItem => !menuItem.toState.includes('billing')
@ -185,6 +187,9 @@ export default {
},
filterMenuItemsByRole(menuItems) {
const { role } = this.currentUser;
if (!role) {
return [];
}
return menuItems.filter(
menuItem =>
window.roleWiseRoutes[role].indexOf(menuItem.toStateName) > -1

View file

@ -8,6 +8,8 @@ export default {
'inbox_conversation',
'settings_account_reports',
'billing_deactivated',
'profile_settings',
'profile_settings_index',
],
menuItems: {
assignedToMe: {

View file

@ -37,11 +37,11 @@
},
"AUTH": {
"TITLE": "Channels",
"DESC": "Currently we support website live chat widgets and Facebook Pages as platforms. We have more platforms like Twitter, Telegram and Line in the works, which will be out soon."
"DESC": "Currently we support Website live chat widgets, Facebook Pages and Twitter profiles as platforms. We have more platforms like Whatsapp, Email, Telegram and Line in the works, which will be out soon."
},
"AGENTS": {
"TITLE": "Agents",
"DESC": "Here you can add agents to manage your newly created inbox. Only these selected agents will have access to your inbox. Agents whcih are not part of this inbox will not be able to see or respond to messages in this inbox when they login. <br> <b>PS:</b> As an administrator, if you need access to all inboxes, you should add yourself as agent to all inboxes that you create."
"DESC": "Here you can add agents to manage your newly created inbox. Only these selected agents will have access to your inbox. Agents which are not part of this inbox will not be able to see or respond to messages in this inbox when they login. <br> <b>PS:</b> As an administrator, if you need access to all inboxes, you should add yourself as agent to all inboxes that you create."
},
"DETAILS": {
"TITLE": "Inbox Details",

View file

@ -10,6 +10,7 @@ import { default as _login } from './login.json';
import { default as _report } from './report.json';
import { default as _resetPassword } from './resetPassword.json';
import { default as _setNewPassword } from './setNewPassword.json';
import { default as _settings } from './settings.json';
import { default as _signup } from './signup.json';
export default {
@ -24,5 +25,6 @@ export default {
..._report,
..._resetPassword,
..._setNewPassword,
..._settings,
..._signup,
};

View file

@ -0,0 +1,50 @@
{
"PROFILE_SETTINGS": {
"LINK": "Profile Settings",
"TITLE": "Profile Settings",
"BTN_TEXT": "Update Profile",
"AFTER_EMAIL_CHANGED": "Your profile has been updated successfully, please login again as your login credentials are changed",
"FORM": {
"AVATAR": "Profile Image",
"ERROR": "Please fix form errors",
"REMOVE_IMAGE": "Remove",
"UPLOAD_IMAGE": "Upload image",
"UPDATE_IMAGE": "Update image",
"PROFILE_SECTION" : {
"TITLE": "Profile",
"NOTE": "Your email address is your identity and is used to log in."
},
"PASSWORD_SECTION" : {
"TITLE": "Password",
"NOTE": "Updating your password would reset your logins in multiple devices."
},
"PROFILE_IMAGE":{
"LABEL": "Profile Image"
},
"NAME": {
"LABEL": "Your name",
"ERROR": "Please enter a valid name",
"PLACEHOLDER": "Please enter your name, this would be displayed in conversations"
},
"EMAIL": {
"LABEL": "Your email address",
"ERROR": "Please enter a valid email address",
"PLACEHOLDER": "Please enter your email address, this would be displayed in conversations"
},
"PASSWORD": {
"LABEL": "Password",
"ERROR": "Please enter a password of length 6 or more",
"PLACEHOLDER": "Please enter a new password"
},
"PASSWORD_CONFIRMATION": {
"LABEL": "Confirm new password",
"ERROR": "Confirm password should match the password",
"PLACEHOLDER": "Please re-enter your password"
}
}
},
"SIDEBAR": {
"PROFILE_SETTINGS": "Profile Settings",
"LOGOUT": "Logout"
}
}

View file

@ -1,23 +1,40 @@
<template>
<form class="login-box medium-4 column align-self-middle" v-on:submit.prevent="login()">
<form
class="login-box medium-4 column align-self-middle"
@submit.prevent="login()"
>
<div class="column log-in-form">
<h4>{{$t('SET_NEW_PASSWORD.TITLE')}}</h4>
<label :class="{ 'error': $v.credentials.password.$error }">
{{$t('LOGIN.PASSWORD.LABEL')}}
<input type="password" v-bind:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')" v-model.trim="credentials.password" @input="$v.credentials.password.$touch">
<span class="message" v-if="$v.credentials.password.$error">
{{$t('SET_NEW_PASSWORD.PASSWORD.ERROR')}}
<h4>{{ $t('SET_NEW_PASSWORD.TITLE') }}</h4>
<label :class="{ error: $v.credentials.password.$error }">
{{ $t('LOGIN.PASSWORD.LABEL') }}
<input
v-model.trim="credentials.password"
type="password"
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
@input="$v.credentials.password.$touch"
/>
<span v-if="$v.credentials.password.$error" class="message">
{{ $t('SET_NEW_PASSWORD.PASSWORD.ERROR') }}
</span>
</label>
<label :class="{ 'error': $v.credentials.confirmPassword.$error }">
{{$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.LABEL')}}
<input type="password" v-bind:placeholder="$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.PLACEHOLDER')" v-model.trim="credentials.confirmPassword" @input="$v.credentials.confirmPassword.$touch">
<span class="message" v-if="$v.credentials.confirmPassword.$error">
{{$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.ERROR')}}
<label :class="{ error: $v.credentials.confirmPassword.$error }">
{{ $t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.LABEL') }}
<input
v-model.trim="credentials.confirmPassword"
type="password"
:placeholder="$t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.PLACEHOLDER')"
@input="$v.credentials.confirmPassword.$touch"
/>
<span v-if="$v.credentials.confirmPassword.$error" class="message">
{{ $t('SET_NEW_PASSWORD.CONFIRM_PASSWORD.ERROR') }}
</span>
</label>
<woot-submit-button
:disabled="$v.credentials.password.$invalid || $v.credentials.confirmPassword.$invalid || newPasswordAPI.showLoading"
:disabled="
$v.credentials.password.$invalid ||
$v.credentials.confirmPassword.$invalid ||
newPasswordAPI.showLoading
"
:button-text="$t('SET_NEW_PASSWORD.SUBMIT')"
:loading="newPasswordAPI.showLoading"
button-class="expanded"
@ -99,7 +116,7 @@ export default {
resetPasswordToken: this.resetPasswordToken,
};
Auth.setNewPassword(credentials)
.then((res) => {
.then(res => {
if (res.status === 200) {
window.location = res.data.redirect_url;
}

View file

@ -7,7 +7,7 @@
<span>{{ headerTitle }}</span>
</h1>
<router-link
v-if="showNewButton && showButton && currentRole"
v-if="showNewButton && showButton && isAdmin"
:to="buttonRoute"
class="button icon success nice button--fixed-right-top"
>
@ -17,18 +17,30 @@
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import BackButton from '../../../components/widgets/BackButton';
import Auth from '../../../api/auth';
export default {
components: {
BackButton,
},
props: {
headerTitle: String,
buttonRoute: String,
buttonText: String,
icon: String,
headerTitle: {
default: '',
type: String,
},
buttonRoute: {
default: '',
type: String,
},
buttonText: {
default: '',
type: String,
},
icon: {
default: '',
type: String,
},
showButton: Boolean,
showNewButton: Boolean,
hideButtonRoutes: {
@ -39,11 +51,14 @@ export default {
},
},
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
}),
iconClass() {
return `icon ${this.icon} header--icon`;
},
currentRole() {
const { role } = Auth.getCurrentUser();
isAdmin() {
const { role } = this.currentUser;
return role === 'administrator';
},
},

View file

@ -108,7 +108,6 @@
/* global bus */
import { mapGetters } from 'vuex';
import md5 from 'md5';
import Thumbnail from '../../../../components/widgets/Thumbnail';
import AddAgent from './AddAgent';
@ -182,10 +181,6 @@ export default {
agent => agent.role === 'administrator' && agent.confirmed
);
},
gravatarUrl(email) {
const hash = md5(email);
return `${window.WootConstants.GRAVATAR_URL}${hash}?default=404`;
},
// Edit Function
openAddPopup() {
this.showAddPopup = true;

View file

@ -0,0 +1,224 @@
<template>
<div class="columns profile--settings ">
<form @submit.prevent="updateUser">
<div class="small-12 row profile--settings--row">
<div class="columns small-3 ">
<p class="section--title">
{{ $t('PROFILE_SETTINGS.FORM.PROFILE_SECTION.TITLE') }}
</p>
<p>{{ $t('PROFILE_SETTINGS.FORM.PROFILE_SECTION.NOTE') }}</p>
</div>
<div class="columns small-9">
<label>
{{ $t('PROFILE_SETTINGS.FORM.PROFILE_IMAGE.LABEL') }}
<thumbnail size="80px" :src="avatarUrl"></thumbnail>
<input
id="file"
ref="file"
type="file"
accept="image/*"
@change="handleImageUpload"
/>
</label>
<label :class="{ error: $v.name.$error }">
{{ $t('PROFILE_SETTINGS.FORM.NAME.LABEL') }}
<input
v-model="name"
type="text"
:placeholder="$t('PROFILE_SETTINGS.FORM.NAME.PLACEHOLDER')"
@input="$v.name.$touch"
/>
<span v-if="$v.name.$error" class="message">
{{ $t('PROFILE_SETTINGS.FORM.NAME.ERROR') }}
</span>
</label>
<label :class="{ error: $v.email.$error }">
{{ $t('PROFILE_SETTINGS.FORM.EMAIL.LABEL') }}
<input
v-model.trim="email"
type="email"
:placeholder="$t('PROFILE_SETTINGS.FORM.EMAIL.PLACEHOLDER')"
@input="$v.email.$touch"
/>
<span v-if="$v.email.$error" class="message">
{{ $t('PROFILE_SETTINGS.FORM.EMAIL.ERROR') }}
</span>
</label>
</div>
</div>
<div class="profile--settings--row row">
<div class="columns small-3 ">
<p class="section--title">
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.TITLE') }}
</p>
<p>{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_SECTION.NOTE') }}</p>
</div>
<div class="columns small-9">
<label :class="{ error: $v.password.$error }">
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD.LABEL') }}
<input
v-model.trim="password"
type="password"
:placeholder="$t('PROFILE_SETTINGS.FORM.PASSWORD.PLACEHOLDER')"
@input="$v.password.$touch"
/>
<span v-if="$v.password.$error" class="message">
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD.ERROR') }}
</span>
</label>
<label :class="{ error: $v.passwordConfirmation.$error }">
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.LABEL') }}
<input
v-model.trim="passwordConfirmation"
type="password"
:placeholder="
$t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.PLACEHOLDER')
"
@input="$v.passwordConfirmation.$touch"
/>
<span v-if="$v.passwordConfirmation.$error" class="message">
{{ $t('PROFILE_SETTINGS.FORM.PASSWORD_CONFIRMATION.ERROR') }}
</span>
</label>
</div>
</div>
<woot-submit-button
class="button nice success button--fixed-right-top"
:button-text="$t('PROFILE_SETTINGS.BTN_TEXT')"
:loading="isUpdating"
>
</woot-submit-button>
</form>
</div>
</template>
<script>
/* global bus */
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import { required, minLength, email } from 'vuelidate/lib/validators';
import { mapGetters } from 'vuex';
import { clearCookiesOnLogout } from '../../../../api/auth';
export default {
components: {
Thumbnail,
},
data() {
return {
avatarFile: '',
avatarUrl: '',
name: '',
email: '',
password: '',
passwordConfirmation: '',
isUpdating: false,
};
},
validations: {
name: {
required,
},
email: {
required,
email,
},
password: {
minLength: minLength(6),
},
passwordConfirmation: {
minLength: minLength(6),
isEqPassword(value) {
if (value !== this.password) {
return false;
}
return true;
},
},
},
computed: {
...mapGetters({
currentUser: 'getCurrentUser',
currentUserId: 'getCurrentUserID',
}),
},
watch: {
currentUserId(newCurrentUserId, prevCurrentUserId) {
if (prevCurrentUserId !== newCurrentUserId) {
this.initializeUser();
}
},
},
mounted() {
if (this.currentUserId) {
this.initializeUser();
}
},
methods: {
initializeUser() {
this.name = this.currentUser.name;
this.email = this.currentUser.email;
this.avatarUrl = this.currentUser.avatar_url;
},
async updateUser() {
this.$v.$touch();
if (this.$v.$invalid) {
bus.$emit('newToastMessage', this.$t('PROFILE_SETTINGS.FORM.ERROR'));
return;
}
this.isUpdating = true;
const hasEmailChanged = this.currentUser.email !== this.email;
try {
await this.$store.dispatch('updateProfile', {
name: this.name,
email: this.email,
avatar: this.avatarFile,
password: this.password,
password_confirmation: this.passwordConfirmation,
});
this.isUpdating = false;
if (hasEmailChanged) {
clearCookiesOnLogout();
bus.$emit(
'newToastMessage',
this.$t('PROFILE_SETTINGS.AFTER_EMAIL_CHANGED')
);
}
} catch (error) {
this.isUpdating = false;
}
},
handleImageUpload(event) {
const [file] = event.target.files;
this.avatarFile = file;
this.avatarUrl = URL.createObjectURL(file);
},
},
};
</script>
<style lang="scss" scoped>
@import '~dashboard/assets/scss/variables.scss';
@import '~dashboard/assets/scss/mixins.scss';
.profile--settings {
padding: 24px;
overflow: auto;
}
.profile--settings--row {
@include border-normal-bottom;
padding: 16px;
.small-3 {
padding: 16px 16px 16px 0;
}
.small-9 {
padding: 16px;
}
}
.section--title {
color: $color-woot;
}
</style>

View file

@ -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'],
},
],
},
],
};

View file

@ -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,
],
};

View file

@ -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);
},
};

View file

@ -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);
});
});
});

View file

@ -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' });
});
});

View file

@ -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');
};

View file

@ -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

View file

@ -21,6 +21,8 @@ module.exports = {
transformIgnorePatterns: ['node_modules/*'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/app/javascript/$1',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/__mocks__/fileMock.js',
},
roots: ['<rootDir>/app/javascript'],
snapshotSerializers: ['jest-serializer-vue'],

View file

@ -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",

View file

@ -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"