diff options
Diffstat (limited to 'apps/settings/src/store/users.js')
-rw-r--r-- | apps/settings/src/store/users.js | 807 |
1 files changed, 807 insertions, 0 deletions
diff --git a/apps/settings/src/store/users.js b/apps/settings/src/store/users.js new file mode 100644 index 00000000000..7e4b9c4aebb --- /dev/null +++ b/apps/settings/src/store/users.js @@ -0,0 +1,807 @@ +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +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 { 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: '', + usercount: 0, + disabled: 0, + canAdd: true, + canRemove: true, + }, +} + +const state = { + users: [], + groups: [ + ...(usersSettings.getSubAdminGroups ?? []), + ...(usersSettings.systemGroups ?? []), + ], + orderBy: usersSettings.sortGroups ?? GroupSorting.UserCount, + minPasswordLength: 0, + usersOffset: 0, + usersLimit: 25, + 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) { + 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 + }, + /** + * @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 === newGroup.id) !== 'undefined') { + return + } + // extend group to default values + const group = Object.assign({}, defaults.group, newGroup) + state.groups.unshift(group) + } catch (e) { + console.error('Can\'t create group', e) + } + }, + renameGroup(state, { gid, displayName }) { + const groupIndex = state.groups.findIndex(groupSearch => groupSearch.id === gid) + if (groupIndex >= 0) { + const updatedGroup = state.groups[groupIndex] + updatedGroup.name = displayName + state.groups.splice(groupIndex, 1, updatedGroup) + } + }, + removeGroup(state, gid) { + const groupIndex = state.groups.findIndex(groupSearch => groupSearch.id === gid) + if (groupIndex >= 0) { + state.groups.splice(groupIndex, 1) + } + }, + addUserGroup(state, { userid, gid }) { + const group = state.groups.find(groupSearch => groupSearch.id === gid) + const user = state.users.find(user => user.id === userid) + // increase count if user is enabled + if (group && user.enabled && state.userCount > 0) { + group.usercount++ + } + const groups = user.groups + groups.push(gid) + }, + removeUserGroup(state, { userid, gid }) { + const group = state.groups.find(groupSearch => groupSearch.id === gid) + const user = state.users.find(user => user.id === userid) + // lower count if user is enabled + if (group && user.enabled && state.userCount > 0) { + group.usercount-- + } + const groups = user.groups + groups.splice(groups.indexOf(gid), 1) + }, + addUserSubAdmin(state, { userid, gid }) { + const groups = state.users.find(user => user.id === userid).subadmin + groups.push(gid) + }, + removeUserSubAdmin(state, { userid, gid }) { + const groups = state.users.find(user => user.id === userid).subadmin + groups.splice(groups.indexOf(gid), 1) + }, + deleteUser(state, userid) { + const userIndex = state.users.findIndex(user => user.id === userid) + this.commit('updateUserCounts', { user: state.users[userIndex], actionType: 'remove' }) + state.users.splice(userIndex, 1) + }, + addUserData(state, response) { + const user = response.data.ocs.data + state.users.unshift(user) + this.commit('updateUserCounts', { user, actionType: 'create' }) + }, + enableDisableUser(state, { userid, enabled }) { + const user = state.users.find(user => user.id === userid) + user.enabled = enabled + this.commit('updateUserCounts', { user, actionType: enabled ? 'enable' : 'disable' }) + }, + // 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 => { + 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 interrupt execution as this is not fatal + } + }, + setUserData(state, { userid, key, value }) { + if (key === 'quota') { + 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 + } + }, + + /** + * Reset users list + * + * @param {object} state the store state + */ + 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) + }) + }, +} + +const getters = { + getUsers(state) { + return state.users + }, + getGroups(state) { + return state.groups + }, + 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 + }, + getUsersOffset(state) { + return state.usersOffset + }, + 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 +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 + * @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 + * @param {string} options.group Get users from group + * @return {Promise} + */ + getUsers(context, { offset, limit, search, group }) { + if (searchRequestCancelSource) { + searchRequestCancelSource.cancel('Operation canceled by another search request.') + } + 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 }), { + cancelToken: searchRequestCancelSource.token, + }) + .then((response) => { + 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) => { + if (!axios.isCancel(error)) { + context.commit('API_FAILURE', error) + } + }) + } + + return api.get(generateOcsUrl('cloud/users/details?offset={offset}&limit={limit}&search={search}', { offset, limit, search }), { + cancelToken: searchRequestCancelSource.token, + }) + .then((response) => { + 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) => { + if (!axios.isCancel(error)) { + context.commit('API_FAILURE', error) + } + }) + }, + + /** + * 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}` + return api.get(generateOcsUrl('cloud/groups?offset={offset}&search={search}', { offset, search }) + limitParam) + .then((response) => { + if (Object.keys(response.data.ocs.data.groups).length > 0) { + response.data.ocs.data.groups.forEach(function(group) { + context.commit('addGroup', { id: group, name: group }) + }) + return true + } + return false + }) + .catch((error) => context.commit('API_FAILURE', error)) + }, + + /** + * Get all 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 - + * @return {Promise} + */ + getUsersFromList(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 })) + .then((response) => { + if (Object.keys(response.data.ocs.data.users).length > 0) { + context.commit('appendUsers', response.data.ocs.data.users) + return true + } + return false + }) + .catch((error) => context.commit('API_FAILURE', error)) + }, + + /** + * Get all users with full details from a groupid + * + * @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.groupid - + * @return {Promise} + */ + getUsersFromGroup(context, { groupid, offset, limit }) { + return api.get(generateOcsUrl('cloud/users/{groupId}/details?offset={offset}&limit={limit}', { groupId: encodeURIComponent(groupid), offset, limit })) + .then((response) => context.commit('getUsersFromList', response.data.ocs.data.users)) + .catch((error) => context.commit('API_FAILURE', error)) + }, + + getPasswordPolicyMinLength(context) { + if (getCapabilities().password_policy && getCapabilities().password_policy.minLength) { + context.commit('setPasswordPolicyMinLength', getCapabilities().password_policy.minLength) + return getCapabilities().password_policy.minLength + } + return false + }, + + /** + * Add group + * + * @param {object} context store context + * @param {string} gid Group id + * @return {Promise} + */ + addGroup(context, gid) { + return api.requireAdmin().then((response) => { + return api.post(generateOcsUrl('cloud/groups'), { groupid: gid }) + .then((response) => { + context.commit('addGroup', { id: gid, name: gid }) + return { gid, displayName: gid } + }) + .catch((error) => { throw error }) + }).catch((error) => { + context.commit('API_FAILURE', { gid, error }) + // let's throw one more time to prevent the view + // from adding the user to a group that doesn't exists + throw error + }) + }, + + /** + * Rename group + * + * @param {object} context store context + * @param {string} groupid Group id + * @param {string} displayName Group display name + * @return {Promise} + */ + renameGroup(context, { groupid, displayName }) { + return api.requireAdmin().then((response) => { + return api.put(generateOcsUrl('cloud/groups/{groupId}', { groupId: encodeURIComponent(groupid) }), { key: 'displayname', value: displayName }) + .then((response) => { + context.commit('renameGroup', { gid: groupid, displayName }) + return { groupid, displayName } + }) + .catch((error) => { throw error }) + }).catch((error) => { + context.commit('API_FAILURE', { groupid, error }) + // let's throw one more time to prevent the view + // from renaming the group + throw error + }) + }, + + /** + * Remove group + * + * @param {object} context store context + * @param {string} gid Group id + * @return {Promise} + */ + removeGroup(context, gid) { + return api.requireAdmin().then((response) => { + return api.delete(generateOcsUrl('cloud/groups/{groupId}', { groupId: encodeURIComponent(gid) })) + .then((response) => context.commit('removeGroup', gid)) + .catch((error) => { throw error }) + }).catch((error) => context.commit('API_FAILURE', { gid, error })) + }, + + /** + * Add user to group + * + * @param {object} context store context + * @param {object} options destructuring object + * @param {string} options.userid User id + * @param {string} options.gid Group id + * @return {Promise} + */ + addUserGroup(context, { userid, gid }) { + return api.requireAdmin().then((response) => { + return api.post(generateOcsUrl('cloud/users/{userid}/groups', { userid }), { groupid: gid }) + .then((response) => context.commit('addUserGroup', { userid, gid })) + .catch((error) => { throw error }) + }).catch((error) => context.commit('API_FAILURE', { userid, error })) + }, + + /** + * Remove user from group + * + * @param {object} context store context + * @param {object} options destructuring object + * @param {string} options.userid User id + * @param {string} options.gid Group id + * @return {Promise} + */ + removeUserGroup(context, { userid, gid }) { + return api.requireAdmin().then((response) => { + return api.delete(generateOcsUrl('cloud/users/{userid}/groups', { userid }), { groupid: gid }) + .then((response) => context.commit('removeUserGroup', { userid, gid })) + .catch((error) => { throw error }) + }).catch((error) => { + context.commit('API_FAILURE', { userid, error }) + // let's throw one more time to prevent + // the view from removing the user row on failure + throw error + }) + }, + + /** + * Add user to group admin + * + * @param {object} context store context + * @param {object} options destructuring object + * @param {string} options.userid User id + * @param {string} options.gid Group id + * @return {Promise} + */ + addUserSubAdmin(context, { userid, gid }) { + return api.requireAdmin().then((response) => { + return api.post(generateOcsUrl('cloud/users/{userid}/subadmins', { userid }), { groupid: gid }) + .then((response) => context.commit('addUserSubAdmin', { userid, gid })) + .catch((error) => { throw error }) + }).catch((error) => context.commit('API_FAILURE', { userid, error })) + }, + + /** + * Remove user from group admin + * + * @param {object} context store context + * @param {object} options destructuring object + * @param {string} options.userid User id + * @param {string} options.gid Group id + * @return {Promise} + */ + removeUserSubAdmin(context, { userid, gid }) { + return api.requireAdmin().then((response) => { + return api.delete(generateOcsUrl('cloud/users/{userid}/subadmins', { userid }), { groupid: gid }) + .then((response) => context.commit('removeUserSubAdmin', { userid, gid })) + .catch((error) => { throw error }) + }).catch((error) => context.commit('API_FAILURE', { userid, error })) + }, + + /** + * Mark all user devices for remote wipe + * + * @param {object} context store context + * @param {string} userid User id + * @return {Promise} + */ + 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')) + } + }, + + /** + * Delete a user + * + * @param {object} context store context + * @param {string} userid User id + * @return {Promise} + */ + deleteUser(context, userid) { + return api.requireAdmin().then((response) => { + return api.delete(generateOcsUrl('cloud/users/{userid}', { userid })) + .then((response) => context.commit('deleteUser', userid)) + .catch((error) => { throw error }) + }).catch((error) => context.commit('API_FAILURE', { userid, error })) + }, + + /** + * Add a user + * + * @param {object} context store context + * @param {Function} context.commit - + * @param {Function} context.dispatch - + * @param {object} options destructuring object + * @param {string} options.userid User id + * @param {string} options.password User password + * @param {string} options.displayName User display name + * @param {string} options.email User email + * @param {string} options.groups User groups + * @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, manager }) { + return api.requireAdmin().then((response) => { + 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) => { + commit('API_FAILURE', { userid, error }) + throw error + }) + }, + + /** + * Get user data and commit addition + * + * @param {object} context store context + * @param {string} userid User id + * @return {Promise} + */ + addUserData(context, userid) { + return api.requireAdmin().then((response) => { + return api.get(generateOcsUrl('cloud/users/{userid}', { userid })) + .then((response) => context.commit('addUserData', response)) + .catch((error) => { throw error }) + }).catch((error) => context.commit('API_FAILURE', { userid, error })) + }, + + /** + * Enable or disable user + * + * @param {object} context store context + * @param {object} options destructuring object + * @param {string} options.userid User id + * @param {boolean} options.enabled User enablement status + * @return {Promise} + */ + enableDisableUser(context, { userid, enabled = true }) { + const userStatus = enabled ? 'enable' : 'disable' + return api.requireAdmin().then((response) => { + return api.put(generateOcsUrl('cloud/users/{userid}/{userStatus}', { userid, userStatus })) + .then((response) => context.commit('enableDisableUser', { userid, enabled })) + .catch((error) => { throw error }) + }).catch((error) => context.commit('API_FAILURE', { userid, error })) + }, + + /** + * Edit user data + * + * @param {object} context store context + * @param {object} options destructuring object + * @param {string} options.userid User id + * @param {string} options.key User field to edit + * @param {string} options.value Value of the change + * @return {Promise} + */ + 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 + } + }, + + /** + * Send welcome mail + * + * @param {object} context store context + * @param {string} userid User id + * @return {Promise} + */ + sendWelcomeMail(context, userid) { + return api.requireAdmin().then((response) => { + return api.post(generateOcsUrl('cloud/users/{userid}/welcome', { userid })) + .then(response => true) + .catch((error) => { throw error }) + }).catch((error) => context.commit('API_FAILURE', { userid, error })) + }, +} + +export default { state, mutations, getters, actions } |