Skip to content

Commit

Permalink
Change own user password (#7206)
Browse files Browse the repository at this point in the history
Password change self service on account info page
  • Loading branch information
Jan committed Jul 4, 2022
1 parent 1275654 commit db0e69f
Show file tree
Hide file tree
Showing 6 changed files with 267 additions and 4 deletions.
7 changes: 7 additions & 0 deletions changelog/unreleased/enhancement-change-own-password
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Enhancement: Add change own password dialog to the account info page

We have added a new change own password dialog to the account info page,
so the user has the possibility to change their own password.

https://github.com/owncloud/web/pull/7206
https://github.com/owncloud/web/issues/7183
5 changes: 5 additions & 0 deletions packages/web-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
MeUserApiFactory,
UsersApiFactory,
GroupsApiFactory,
MeChangepasswordApiFactory,
Group,
CollectionOfGroup,
CollectionOfUser,
Expand All @@ -28,6 +29,7 @@ export interface Graph {
getUser: (userId: string) => AxiosPromise<User>
createUser: (user: User) => AxiosPromise<User>
getMe: () => AxiosPromise<User>
changeOwnPassword: (currentPassword: string, newPassword: string) => AxiosPromise<void>
editUser: (userId: string, user: User) => AxiosPromise<User>
deleteUser: (userId: string) => AxiosPromise<void>
listUsers: (orderBy?: string) => AxiosPromise<CollectionOfUser>
Expand All @@ -50,6 +52,7 @@ const graph = (baseURI: string, axiosClient: AxiosInstance): Graph => {

const meDrivesApi = new MeDrivesApi(config, config.basePath, axiosClient)
const meUserApiFactory = MeUserApiFactory(config, config.basePath, axiosClient)
const meChangepasswordApi = MeChangepasswordApiFactory(config, config.basePath, axiosClient)
const userApiFactory = UserApiFactory(config, config.basePath, axiosClient)
const usersApiFactory = UsersApiFactory(config, config.basePath, axiosClient)
const groupApiFactory = GroupApiFactory(config, config.basePath, axiosClient)
Expand All @@ -72,6 +75,8 @@ const graph = (baseURI: string, axiosClient: AxiosInstance): Graph => {
getUser: (userId: string) => userApiFactory.getUser(userId),
createUser: (user: User) => usersApiFactory.createUser(user),
getMe: () => meUserApiFactory.meGet(),
changeOwnPassword: (currentPassword, newPassword) =>
meChangepasswordApi.changeOwnPassword({ currentPassword, newPassword }),
editUser: (userId: string, user: User) => userApiFactory.updateUser(userId, user),
deleteUser: (userId: string) => userApiFactory.deleteUser(userId),
listUsers: (orderBy?: any) =>
Expand Down
78 changes: 78 additions & 0 deletions packages/web-runtime/src/components/EditPasswordModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<template>
<oc-modal
:title="$gettext('Change password')"
:button-cancel-text="$gettext('Cancel')"
:button-confirm-text="$gettext('Confirm')"
:button-confirm-disabled="confirmButtonDisabled"
@confirm="editPassword"
@cancel="$emit('cancel')"
>
<template #content>
<oc-text-input
v-model="currentPassword"
:label="$gettext('Current password')"
type="password"
:fix-message-line="true"
/>
<oc-text-input
v-model="newPassword"
:label="$gettext('New password')"
type="password"
:fix-message-line="true"
@change="validatePasswordConfirm"
/>
<oc-text-input
v-model="newPasswordConfirm"
:label="$gettext('Repeat new password')"
type="password"
:fix-message-line="true"
:error-message="passwordConfirmErrorMessage"
@change="validatePasswordConfirm"
/>
</template>
</oc-modal>
</template>

<script>
export default {
name: 'EditPasswordModal',
data: function () {
return {
currentPassword: '',
newPassword: '',
newPasswordConfirm: '',
passwordConfirmErrorMessage: ''
}
},
computed: {
confirmButtonDisabled() {
return (
!this.currentPassword.trim().length ||
!this.newPassword.trim().length ||
this.newPassword !== this.newPasswordConfirm
)
}
},
methods: {
editPassword() {
this.$emit('confirm', this.currentPassword, this.newPassword)
},
validatePasswordConfirm() {
this.passwordConfirmErrorMessage = ''
if (
this.newPassword.trim().length &&
this.newPasswordConfirm.trim().length &&
this.newPassword !== this.newPasswordConfirm
) {
this.passwordConfirmErrorMessage = this.$gettext(
'Password and password confirmation must be identical'
)
return false
}
return true
}
}
}
</script>
63 changes: 60 additions & 3 deletions packages/web-runtime/src/pages/account.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
<div class="oc-flex oc-flex-between oc-flex-bottom oc-width-1-1 oc-border-b oc-py">
<h1 id="account-page-title" class="oc-page-title oc-m-rm">{{ pageTitle }}</h1>
<div>
<edit-password-modal
v-if="editPasswordModalOpen"
@cancel="closeEditPasswordModal"
@confirm="editPassword"
></edit-password-modal>
<oc-button
v-if="isChangePasswordEnabled"
variation="primary"
data-testid="account-page-edit-url-btn"
@click="showEditPasswordModal"
>
<oc-icon name="lock" />
<translate>Change Password</translate>
</oc-button>
<oc-button
v-if="editUrl"
variation="primary"
Expand Down Expand Up @@ -72,20 +86,38 @@
</template>

<script>
import { mapGetters } from 'vuex'
import { mapActions, mapGetters } from 'vuex'
import EditPasswordModal from '../components/EditPasswordModal.vue'
import { clientService } from 'web-pkg/src/services'
export default {
name: 'Personal',
components: {
EditPasswordModal
},
data() {
return {
loadingGroups: true,
groups: []
groups: [],
editPasswordModalOpen: false
}
},
computed: {
...mapGetters(['user', 'configuration', 'getNavItemsByExtension', 'apps']),
...mapGetters([
'user',
'configuration',
'getNavItemsByExtension',
'apps',
'capabilities',
'getToken'
]),
isAccountEditingEnabled() {
return !this.apps.settings
},
isChangePasswordEnabled() {
// FIXME: spaces capability is not correct here, we need to retrieve an appropriate capability
return this.capabilities.spaces?.enabled
},
pageTitle() {
return this.$gettext(this.$route.meta.title)
},
Expand All @@ -110,9 +142,34 @@ export default {
this.loadGroups()
},
methods: {
...mapActions(['showMessage']),
async loadGroups() {
this.groups = await this.$client.users.getUserGroups(this.user.id)
this.loadingGroups = false
},
showEditPasswordModal() {
this.editPasswordModalOpen = true
},
closeEditPasswordModal() {
this.editPasswordModalOpen = false
},
editPassword(currentPassword, newPassword) {
const graphClient = clientService.graphAuthenticated(this.configuration.server, this.getToken)
return graphClient.users
.changeOwnPassword(currentPassword.trim(), newPassword.trim())
.then(() => {
this.closeEditPasswordModal()
this.showMessage({
title: this.$gettext('Password was changed successfully')
})
})
.catch((error) => {
console.error(error)
this.showMessage({
title: this.$gettext('Failed to change password'),
status: 'danger'
})
})
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Vuex from 'vuex'
import { mount, createLocalVue } from '@vue/test-utils'
import EditPasswordModal from '../../../src/components/EditPasswordModal'

const localVue = createLocalVue()
localVue.use(Vuex)

afterEach(() => jest.clearAllMocks())

describe('EditPasswordModal', () => {
describe('computed method "confirmButtonDisabled"', () => {
it('should be true if any data set is invalid', () => {
const wrapper = getWrapper()
wrapper.vm.currentPassword = ''
expect(wrapper.vm.confirmButtonDisabled).toBeTruthy()
})
it('should be false if no data set is invalid', () => {
const wrapper = getWrapper()
wrapper.vm.currentPassword = 'password'
wrapper.vm.newPassword = 'newpassword'
wrapper.vm.newPasswordConfirm = 'newpassword'
expect(wrapper.vm.confirmButtonDisabled).toBeFalsy()
})
})

describe('method "validatePasswordConfirm"', () => {
it('should be true if passwords are identical', () => {
const wrapper = getWrapper()
wrapper.vm.newPassword = 'newpassword'
wrapper.vm.newPasswordConfirm = 'newpassword'
expect(wrapper.vm.validatePasswordConfirm).toBeTruthy()
})
it('should be false if passwords are not identical', () => {
const wrapper = getWrapper()
wrapper.vm.newPassword = 'newpassword'
wrapper.vm.newPasswordConfirm = 'anothernewpassword'
expect(wrapper.vm.validatePasswordConfirm).toBeTruthy()
})
})
})

function getWrapper() {
return mount(EditPasswordModal, {
localVue,
mocks: {
$gettext: jest.fn(),
$gettextInterpolate: jest.fn()
},
propsData: {
cancel: jest.fn(),
confirm: jest.fn(),
existingGroups: [
{
displayName: 'admins'
}
]
},
stubs: { 'oc-modal': true, 'oc-text-input': true }
})
}
58 changes: 57 additions & 1 deletion packages/web-runtime/tests/unit/pages/account.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Vuex from 'vuex'
import { createStore } from 'vuex-extensions'
import GetTextPlugin from 'vue-gettext'
import VueCompositionAPI from '@vue/composition-api'
import mockAxios from 'jest-mock-axios'

const localVue = createLocalVue()
localVue.use(Vuex)
Expand Down Expand Up @@ -123,6 +124,50 @@ describe('account page', () => {
})
})
})

describe('method "editPassword"', () => {
it('should show message on success', async () => {
mockAxios.request.mockImplementationOnce(() => {
return Promise.resolve()
})
const store = getStore({ server: 'https://example.com' })
const wrapper = getWrapper(store)

const showMessageStub = jest.spyOn(wrapper.vm, 'showMessage')

await wrapper.vm.editPassword('password', 'newPassword')

expect(showMessageStub).toHaveBeenCalled()
})

it('should show message on error', async () => {
mockAxios.request.mockImplementationOnce(() => {
return Promise.reject(new Error())
})
const store = getStore({ server: 'https://example.com' })
const wrapper = getWrapper(store)

jest.spyOn(console, 'error').mockImplementation(() => {})
const showMessageStub = jest.spyOn(wrapper.vm, 'showMessage')

await wrapper.vm.editPassword('password', 'newPassword')

expect(showMessageStub).toHaveBeenCalled()
})
})

describe('computed method "isChangePasswordEnabled"', () => {
it('should be true if capability is enabled', () => {
const store = getStore({ capabilities: { spaces: { enabled: true } } })
const wrapper = getWrapper(store)
expect(wrapper.vm.isChangePasswordEnabled).toBeTruthy()
})
it('should be false if capability is not enabled', () => {
const store = getStore()
const wrapper = getWrapper(store)
expect(wrapper.vm.isChangePasswordEnabled).toBeFalsy()
})
})
})

function getWrapper(store = getStore()) {
Expand All @@ -149,14 +194,25 @@ function getStore({
user = {},
server = '',
getNavItemsByExtension = jest.fn(() => []),
isAccountEditingEnabled = true
isAccountEditingEnabled = true,
capabilities = {}
} = {}) {
return createStore(Vuex.Store, {
actions: {
createModal: jest.fn(),
hideModal: jest.fn(),
showMessage: jest.fn(),
setModalInputErrorMessage: jest.fn()
},
getters: {
user: () => user,
configuration: () => ({
server: server
}),
getToken: () => 'token',
capabilities: () => {
return capabilities
},
getNavItemsByExtension: () => getNavItemsByExtension,
apps: () => ({
...(isAccountEditingEnabled || { settings: {} })
Expand Down

0 comments on commit db0e69f

Please sign in to comment.