diff options
Diffstat (limited to 'apps/settings/src/store/users.js')
-rw-r--r-- | apps/settings/src/store/users.js | 370 |
1 files changed, 269 insertions, 101 deletions
diff --git a/apps/settings/src/store/users.js b/apps/settings/src/store/users.js index ec15c8dd745..7e4b9c4aebb 100644 --- a/apps/settings/src/store/users.js +++ b/apps/settings/src/store/users.js @@ -1,49 +1,29 @@ /** - * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ -import api from './api' +import { getBuilder } from '@nextcloud/browser-storage' +import { getCapabilities } from '@nextcloud/capabilities' +import { parseFileSize } from '@nextcloud/files' +import { showError } from '@nextcloud/dialogs' +import { generateOcsUrl, generateUrl } from '@nextcloud/router' +import { loadState } from '@nextcloud/initial-state' import axios from '@nextcloud/axios' -import { generateOcsUrl } from '@nextcloud/router' -import logger from '../logger' -const orderGroups = function(groups, orderBy) { - /* const SORT_USERCOUNT = 1; - * const SORT_GROUPNAME = 2; - * https://github.com/nextcloud/server/blob/208e38e84e1a07a49699aa90dc5b7272d24489f0/lib/private/Group/MetaData.php#L34 - */ - if (orderBy === 1) { - return groups.sort((a, b) => a.usercount - a.disabled < b.usercount - b.disabled) - } else { - return groups.sort((a, b) => a.name.localeCompare(b.name)) - } -} +import { GroupSorting } from '../constants/GroupManagement.ts' +import { naturalCollator } from '../utils/sorting.ts' +import api from './api.js' +import logger from '../logger.ts' + +const usersSettings = loadState('settings', 'usersSettings', {}) + +const localStorage = getBuilder('settings').persist(true).build() const defaults = { + /** + * @type {import('../views/user-types').IGroup} + */ group: { id: '', name: '', @@ -56,43 +36,55 @@ const defaults = { const state = { users: [], - groups: [], - orderBy: 1, + groups: [ + ...(usersSettings.getSubAdminGroups ?? []), + ...(usersSettings.systemGroups ?? []), + ], + orderBy: usersSettings.sortGroups ?? GroupSorting.UserCount, minPasswordLength: 0, usersOffset: 0, usersLimit: 25, - userCount: 0, + disabledUsersOffset: 0, + disabledUsersLimit: 25, + userCount: usersSettings.userCount ?? 0, + showConfig: { + showStoragePath: localStorage.getItem('account_settings__showStoragePath') === 'true', + showUserBackend: localStorage.getItem('account_settings__showUserBackend') === 'true', + showFirstLogin: localStorage.getItem('account_settings__showFirstLogin') === 'true', + showLastLogin: localStorage.getItem('account_settings__showLastLogin') === 'true', + showNewUserForm: localStorage.getItem('account_settings__showNewUserForm') === 'true', + showLanguages: localStorage.getItem('account_settings__showLanguages') === 'true', + }, } const mutations = { appendUsers(state, usersObj) { - // convert obj to array - const users = state.users.concat(Object.keys(usersObj).map(userid => usersObj[userid])) + const existingUsers = state.users.map(({ id }) => id) + const newUsers = Object.values(usersObj) + .filter(({ id }) => !existingUsers.includes(id)) + + const users = state.users.concat(newUsers) state.usersOffset += state.usersLimit state.users = users }, + updateDisabledUsers(state, _usersObj) { + state.disabledUsersOffset += state.disabledUsersLimit + }, setPasswordPolicyMinLength(state, length) { state.minPasswordLength = length !== '' ? length : 0 }, - initGroups(state, { groups, orderBy, userCount }) { - state.groups = groups.map(group => Object.assign({}, defaults.group, group)) - state.orderBy = orderBy - state.userCount = userCount - state.groups = orderGroups(state.groups, state.orderBy) - - }, - addGroup(state, { gid, displayName }) { + /** + * @param {object} state store state + * @param {import('../views/user-types.js').IGroup} newGroup new group + */ + addGroup(state, newGroup) { try { - if (typeof state.groups.find((group) => group.id === gid) !== 'undefined') { + if (typeof state.groups.find((group) => group.id === newGroup.id) !== 'undefined') { return } // extend group to default values - const group = Object.assign({}, defaults.group, { - id: gid, - name: displayName, - }) - state.groups.push(group) - state.groups = orderGroups(state.groups, state.orderBy) + const group = Object.assign({}, defaults.group, newGroup) + state.groups.unshift(group) } catch (e) { console.error('Can\'t create group', e) } @@ -103,7 +95,6 @@ const mutations = { const updatedGroup = state.groups[groupIndex] updatedGroup.name = displayName state.groups.splice(groupIndex, 1, updatedGroup) - state.groups = orderGroups(state.groups, state.orderBy) } }, removeGroup(state, gid) { @@ -121,7 +112,6 @@ const mutations = { } const groups = user.groups groups.push(gid) - state.groups = orderGroups(state.groups, state.orderBy) }, removeUserGroup(state, { userid, gid }) { const group = state.groups.find(groupSearch => groupSearch.id === gid) @@ -132,7 +122,6 @@ const mutations = { } const groups = user.groups groups.splice(groups.indexOf(gid), 1) - state.groups = orderGroups(state.groups, state.orderBy) }, addUserSubAdmin(state, { userid, gid }) { const groups = state.users.find(user => user.id === userid).subadmin @@ -149,7 +138,7 @@ const mutations = { }, addUserData(state, response) { const user = response.data.ocs.data - state.users.push(user) + state.users.unshift(user) this.commit('updateUserCounts', { user, actionType: 'create' }) }, enableDisableUser(state, { userid, enabled }) { @@ -159,49 +148,70 @@ const mutations = { }, // update active/disabled counts, groups counts updateUserCounts(state, { user, actionType }) { + // 0 is a special value + if (state.userCount === 0) { + return + } + + const recentGroup = state.groups.find(group => group.id === '__nc_internal_recent') const disabledGroup = state.groups.find(group => group.id === 'disabled') switch (actionType) { case 'enable': case 'disable': disabledGroup.usercount += user.enabled ? -1 : 1 // update Disabled Users count + recentGroup.usercount += user.enabled ? 1 : -1 state.userCount += user.enabled ? 1 : -1 // update Active Users count user.groups.forEach(userGroup => { const group = state.groups.find(groupSearch => groupSearch.id === userGroup) + if (!group) { + return + } group.disabled += user.enabled ? -1 : 1 // update group disabled count }) break case 'create': + recentGroup.usercount++ state.userCount++ // increment Active Users count user.groups.forEach(userGroup => { - state.groups - .find(groupSearch => groupSearch.id === userGroup) - .usercount++ // increment group total count + const group = state.groups.find(groupSearch => groupSearch.id === userGroup) + if (!group) { + return + } + group.usercount++ // increment group total count }) break case 'remove': if (user.enabled) { + recentGroup.usercount-- state.userCount-- // decrement Active Users count user.groups.forEach(userGroup => { const group = state.groups.find(groupSearch => groupSearch.id === userGroup) + if (!group) { + console.warn('User group ' + userGroup + ' does not exist during user removal') + return + } group.usercount-- // decrement group total count }) } else { disabledGroup.usercount-- // decrement Disabled Users count user.groups.forEach(userGroup => { const group = state.groups.find(groupSearch => groupSearch.id === userGroup) + if (!group) { + return + } group.disabled-- // decrement group disabled count }) } break default: logger.error(`Unknown action type in updateUserCounts: '${actionType}'`) - // not throwing error to interupt execution as this is not fatal + // not throwing error to interrupt execution as this is not fatal } }, setUserData(state, { userid, key, value }) { if (key === 'quota') { - const humanValue = OC.Util.computerFileSize(value) + const humanValue = parseFileSize(value, true) state.users.find(user => user.id === userid)[key][key] = humanValue !== null ? humanValue : value } else { state.users.find(user => user.id === userid)[key] = value @@ -216,6 +226,41 @@ const mutations = { resetUsers(state) { state.users = [] state.usersOffset = 0 + state.disabledUsersOffset = 0 + }, + + /** + * Reset group list + * + * @param {object} state the store state + */ + resetGroups(state) { + state.groups = [ + ...(usersSettings.getSubAdminGroups ?? []), + ...(usersSettings.systemGroups ?? []), + ] + }, + + setShowConfig(state, { key, value }) { + localStorage.setItem(`account_settings__${key}`, JSON.stringify(value)) + state.showConfig[key] = value + }, + + setGroupSorting(state, sorting) { + const oldValue = state.orderBy + state.orderBy = sorting + + // Persist the value on the server + axios.post( + generateUrl('/settings/users/preferences/group.sortBy'), + { + value: String(sorting), + }, + ).catch((error) => { + state.orderBy = oldValue + showError(t('settings', 'Could not set group sorting')) + logger.error(error) + }) }, } @@ -226,9 +271,24 @@ const getters = { getGroups(state) { return state.groups }, - getSubadminGroups(state) { - // Can't be subadmin of admin or disabled - return state.groups.filter(group => group.id !== 'admin' && group.id !== 'disabled') + getSubAdminGroups() { + return usersSettings.subAdminGroups ?? [] + }, + + getSortedGroups(state) { + const groups = [...state.groups] + if (state.orderBy === GroupSorting.UserCount) { + return groups.sort((a, b) => { + const numA = a.usercount - a.disabled + const numB = b.usercount - b.disabled + return (numA < numB) ? 1 : (numB < numA ? -1 : naturalCollator.compare(a.name, b.name)) + }) + } else { + return groups.sort((a, b) => naturalCollator.compare(a.name, b.name)) + } + }, + getGroupSorting(state) { + return state.orderBy }, getPasswordPolicyMinLength(state) { return state.minPasswordLength @@ -239,9 +299,18 @@ const getters = { getUsersLimit(state) { return state.usersLimit }, + getDisabledUsersOffset(state) { + return state.disabledUsersOffset + }, + getDisabledUsersLimit(state) { + return state.disabledUsersLimit + }, getUserCount(state) { return state.userCount }, + getShowConfig(state) { + return state.showConfig + }, } const CancelToken = axios.CancelToken @@ -250,6 +319,41 @@ let searchRequestCancelSource = null const actions = { /** + * search users + * + * @param {object} context store context + * @param {object} options destructuring object + * @param {number} options.offset List offset to request + * @param {number} options.limit List number to return from offset + * @param {string} options.search Search amongst users + * @return {Promise} + */ + searchUsers(context, { offset, limit, search }) { + search = typeof search === 'string' ? search : '' + + return api.get(generateOcsUrl('cloud/users/details?offset={offset}&limit={limit}&search={search}', { offset, limit, search })).catch((error) => { + if (!axios.isCancel(error)) { + context.commit('API_FAILURE', error) + } + }) + }, + + /** + * Get user details + * + * @param {object} context store context + * @param {string} userId user id + * @return {Promise} + */ + getUser(context, userId) { + return api.get(generateOcsUrl(`cloud/users/${userId}`)).catch((error) => { + if (!axios.isCancel(error)) { + context.commit('API_FAILURE', error) + } + }) + }, + + /** * Get all users with full details * * @param {object} context store context @@ -266,6 +370,14 @@ const actions = { } searchRequestCancelSource = CancelToken.source() search = typeof search === 'string' ? search : '' + + /** + * Adding filters in the search bar such as in:files, in:users, etc. + * collides with this particular search, so we need to remove them + * here and leave only the original search query + */ + search = search.replace(/in:[^\s]+/g, '').trim() + group = typeof group === 'string' ? group : '' if (group !== '') { return api.get(generateOcsUrl('cloud/groups/{group}/users/details?offset={offset}&limit={limit}&search={search}', { group: encodeURIComponent(group), offset, limit, search }), { @@ -302,6 +414,55 @@ const actions = { }) }, + /** + * Get recent users with full details + * + * @param {object} context store context + * @param {object} options destructuring object + * @param {number} options.offset List offset to request + * @param {number} options.limit List number to return from offset + * @param {string} options.search Search query + * @return {Promise<number>} + */ + async getRecentUsers(context, { offset, limit, search }) { + const url = generateOcsUrl('cloud/users/recent?offset={offset}&limit={limit}&search={search}', { offset, limit, search }) + try { + const response = await api.get(url) + const usersCount = Object.keys(response.data.ocs.data.users).length + if (usersCount > 0) { + context.commit('appendUsers', response.data.ocs.data.users) + } + return usersCount + } catch (error) { + context.commit('API_FAILURE', error) + } + }, + + /** + * Get disabled users with full details + * + * @param {object} context store context + * @param {object} options destructuring object + * @param {number} options.offset List offset to request + * @param {number} options.limit List number to return from offset + * @param options.search + * @return {Promise<number>} + */ + async getDisabledUsers(context, { offset, limit, search }) { + const url = generateOcsUrl('cloud/users/disabled?offset={offset}&limit={limit}&search={search}', { offset, limit, search }) + try { + const response = await api.get(url) + const usersCount = Object.keys(response.data.ocs.data.users).length + if (usersCount > 0) { + context.commit('appendUsers', response.data.ocs.data.users) + context.commit('updateDisabledUsers', response.data.ocs.data.users) + } + return usersCount + } catch (error) { + context.commit('API_FAILURE', error) + } + }, + getGroups(context, { offset, limit, search }) { search = typeof search === 'string' ? search : '' const limitParam = limit === -1 ? '' : `&limit=${limit}` @@ -309,7 +470,7 @@ const actions = { .then((response) => { if (Object.keys(response.data.ocs.data.groups).length > 0) { response.data.ocs.data.groups.forEach(function(group) { - context.commit('addGroup', { gid: group, displayName: group }) + context.commit('addGroup', { id: group, name: group }) }) return true } @@ -358,9 +519,9 @@ const actions = { }, getPasswordPolicyMinLength(context) { - if (OC.getCapabilities().password_policy && OC.getCapabilities().password_policy.minLength) { - context.commit('setPasswordPolicyMinLength', OC.getCapabilities().password_policy.minLength) - return OC.getCapabilities().password_policy.minLength + if (getCapabilities().password_policy && getCapabilities().password_policy.minLength) { + context.commit('setPasswordPolicyMinLength', getCapabilities().password_policy.minLength) + return getCapabilities().password_policy.minLength } return false }, @@ -376,7 +537,7 @@ const actions = { return api.requireAdmin().then((response) => { return api.post(generateOcsUrl('cloud/groups'), { groupid: gid }) .then((response) => { - context.commit('addGroup', { gid, displayName: gid }) + context.commit('addGroup', { id: gid, name: gid }) return { gid, displayName: gid } }) .catch((error) => { throw error }) @@ -391,7 +552,7 @@ const actions = { /** * Rename group * - * @param {Object} context store context + * @param {object} context store context * @param {string} groupid Group id * @param {string} displayName Group display name * @return {Promise} @@ -507,11 +668,14 @@ const actions = { * @param {string} userid User id * @return {Promise} */ - wipeUserDevices(context, userid) { - return api.requireAdmin().then((response) => { - return api.post(generateOcsUrl('cloud/users/{userid}/wipe', { userid })) - .catch((error) => { throw error }) - }).catch((error) => context.commit('API_FAILURE', { userid, error })) + async wipeUserDevices(context, userid) { + try { + await api.requireAdmin() + return await api.post(generateOcsUrl('cloud/users/{userid}/wipe', { userid })) + } catch (error) { + context.commit('API_FAILURE', { userid, error }) + return Promise.reject(new Error('Failed to wipe user devices')) + } }, /** @@ -544,11 +708,12 @@ const actions = { * @param {string} options.subadmin User subadmin groups * @param {string} options.quota User email * @param {string} options.language User language + * @param {string} options.manager User manager * @return {Promise} */ - addUser({ commit, dispatch }, { userid, password, displayName, email, groups, subadmin, quota, language }) { + addUser({ commit, dispatch }, { userid, password, displayName, email, groups, subadmin, quota, language, manager }) { return api.requireAdmin().then((response) => { - return api.post(generateOcsUrl('cloud/users'), { userid, password, displayName, email, groups, subadmin, quota, language }) + return api.post(generateOcsUrl('cloud/users'), { userid, password, displayName, email, groups, subadmin, quota, language, manager }) .then((response) => dispatch('addUserData', userid || response.data.ocs.data.id)) .catch((error) => { throw error }) }).catch((error) => { @@ -600,24 +765,27 @@ const actions = { * @param {string} options.value Value of the change * @return {Promise} */ - setUserData(context, { userid, key, value }) { - const allowedEmpty = ['email', 'displayname'] - if (['email', 'language', 'quota', 'displayname', 'password'].indexOf(key) !== -1) { - // We allow empty email or displayname - if (typeof value === 'string' - && ( - (allowedEmpty.indexOf(key) === -1 && value.length > 0) - || allowedEmpty.indexOf(key) !== -1 - ) - ) { - return api.requireAdmin().then((response) => { - return api.put(generateOcsUrl('cloud/users/{userid}', { userid }), { key, value }) - .then((response) => context.commit('setUserData', { userid, key, value })) - .catch((error) => { throw error }) - }).catch((error) => context.commit('API_FAILURE', { userid, error })) - } + async setUserData(context, { userid, key, value }) { + const allowedEmpty = ['email', 'displayname', 'manager'] + const validKeys = ['email', 'language', 'quota', 'displayname', 'password', 'manager'] + + if (!validKeys.includes(key)) { + throw new Error('Invalid request data') + } + + // If value is empty and the key doesn't allow empty values, throw error + if (value === '' && !allowedEmpty.includes(key)) { + throw new Error('Value cannot be empty for this field') + } + + try { + await api.requireAdmin() + await api.put(generateOcsUrl('cloud/users/{userid}', { userid }), { key, value }) + return context.commit('setUserData', { userid, key, value }) + } catch (error) { + context.commit('API_FAILURE', { userid, error }) + throw error } - return Promise.reject(new Error('Invalid request data')) }, /** |