aboutsummaryrefslogtreecommitdiffstats
path: root/apps/settings/src/store
diff options
context:
space:
mode:
Diffstat (limited to 'apps/settings/src/store')
-rw-r--r--apps/settings/src/store/admin-security.js24
-rw-r--r--apps/settings/src/store/api.js26
-rw-r--r--apps/settings/src/store/app-api-store.ts325
-rw-r--r--apps/settings/src/store/apps-store.ts87
-rw-r--r--apps/settings/src/store/apps.js113
-rw-r--r--apps/settings/src/store/authtoken.ts200
-rw-r--r--apps/settings/src/store/index.js66
-rw-r--r--apps/settings/src/store/oc.js35
-rw-r--r--apps/settings/src/store/settings.js38
-rw-r--r--apps/settings/src/store/users-settings.js23
-rw-r--r--apps/settings/src/store/users.js584
11 files changed, 1168 insertions, 353 deletions
diff --git a/apps/settings/src/store/admin-security.js b/apps/settings/src/store/admin-security.js
index c71f40a1e85..2cb5eb101ef 100644
--- a/apps/settings/src/store/admin-security.js
+++ b/apps/settings/src/store/admin-security.js
@@ -1,26 +1,10 @@
/**
- * @copyright 2019 Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @author 2019 Roeland Jago Douma <roeland@famdouma.nl>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * 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: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
*/
import Vue from 'vue'
-import Vuex from 'vuex'
+import Vuex, { Store } from 'vuex'
Vue.use(Vuex)
@@ -42,7 +26,7 @@ const mutations = {
},
}
-export default new Vuex.Store({
+export default new Store({
strict: process.env.NODE_ENV !== 'production',
state,
mutations,
diff --git a/apps/settings/src/store/api.js b/apps/settings/src/store/api.js
index 9a03fe68ef9..f36d44cc5c0 100644
--- a/apps/settings/src/store/api.js
+++ b/apps/settings/src/store/api.js
@@ -1,27 +1,11 @@
/**
- * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * 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 axios from '@nextcloud/axios'
-import confirmPassword from '@nextcloud/password-confirmation'
+import { confirmPassword } from '@nextcloud/password-confirmation'
+import '@nextcloud/password-confirmation/dist/style.css'
const sanitize = function(url) {
return url.replace(/\/$/, '') // Remove last url slash
@@ -58,7 +42,7 @@ export default {
* .catch((error) => {throw error;});
* }).catch((error) => {requireAdmin OR API failure});
*
- * @returns {Promise}
+ * @return {Promise}
*/
requireAdmin() {
return confirmPassword()
diff --git a/apps/settings/src/store/app-api-store.ts b/apps/settings/src/store/app-api-store.ts
new file mode 100644
index 00000000000..769f212ebd7
--- /dev/null
+++ b/apps/settings/src/store/app-api-store.ts
@@ -0,0 +1,325 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import axios from '@nextcloud/axios'
+import { confirmPassword } from '@nextcloud/password-confirmation'
+import { showError, showInfo } from '@nextcloud/dialogs'
+import { loadState } from '@nextcloud/initial-state'
+import { translate as t } from '@nextcloud/l10n'
+import { generateUrl } from '@nextcloud/router'
+import { defineStore } from 'pinia'
+
+import api from './api'
+import logger from '../logger'
+
+import type { IAppstoreExApp, IDeployDaemon, IDeployOptions, IExAppStatus } from '../app-types.ts'
+import Vue from 'vue'
+
+interface AppApiState {
+ apps: IAppstoreExApp[]
+ updateCount: number
+ loading: Record<string, boolean>
+ loadingList: boolean
+ statusUpdater: number | null | undefined
+ daemonAccessible: boolean
+ defaultDaemon: IDeployDaemon | null
+ dockerDaemons: IDeployDaemon[]
+}
+
+export const useAppApiStore = defineStore('app-api-apps', {
+ state: (): AppApiState => ({
+ apps: [],
+ updateCount: loadState('settings', 'appstoreExAppUpdateCount', 0),
+ loading: {},
+ loadingList: false,
+ statusUpdater: null,
+ daemonAccessible: loadState('settings', 'defaultDaemonConfigAccessible', false),
+ defaultDaemon: loadState('settings', 'defaultDaemonConfig', null),
+ dockerDaemons: [],
+ }),
+
+ getters: {
+ getLoading: (state) => (id: string) => state.loading[id] ?? false,
+ getAllApps: (state) => state.apps,
+ getUpdateCount: (state) => state.updateCount,
+ getDaemonAccessible: (state) => state.daemonAccessible,
+ getDefaultDaemon: (state) => state.defaultDaemon,
+ getAppStatus: (state) => (appId: string) =>
+ state.apps.find((app) => app.id === appId)?.status || null,
+ getStatusUpdater: (state) => state.statusUpdater,
+ getInitializingOrDeployingApps: (state) =>
+ state.apps.filter((app) =>
+ app?.status?.action
+ && (app?.status?.action === 'deploy' || app.status.action === 'init' || app.status.action === 'healthcheck')
+ && app.status.type !== '',
+ ),
+ },
+
+ actions: {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ appsApiFailure(error: any) {
+ showError(t('settings', 'An error occurred during the request. Unable to proceed.') + '<br>' + error.error.response.data.data.message, { isHTML: true })
+ logger.error(error)
+ },
+
+ setLoading(id: string, value: boolean) {
+ Vue.set(this.loading, id, value)
+ },
+
+ setError(appId: string | string[], error: string) {
+ const appIds = Array.isArray(appId) ? appId : [appId]
+ appIds.forEach((_id) => {
+ const app = this.apps.find((app) => app.id === _id)
+ if (app) {
+ app.error = error
+ }
+ })
+ },
+
+ enableApp(appId: string, daemon: IDeployDaemon, deployOptions: IDeployOptions) {
+ this.setLoading(appId, true)
+ this.setLoading('install', true)
+ return confirmPassword().then(() => {
+
+ return axios.post(generateUrl(`/apps/app_api/apps/enable/${appId}/${daemon.name}`), { deployOptions })
+ .then((response) => {
+ this.setLoading(appId, false)
+ this.setLoading('install', false)
+
+ const app = this.apps.find((app) => app.id === appId)
+ if (app) {
+ if (!app.installed) {
+ app.installed = true
+ app.needsDownload = false
+ app.daemon = daemon
+ app.status = {
+ type: 'install',
+ action: 'deploy',
+ init: 0,
+ deploy: 0,
+ } as IExAppStatus
+ }
+ app.active = true
+ app.canUnInstall = false
+ app.removable = true
+ app.error = ''
+ }
+
+ this.updateAppsStatus()
+
+ return axios.get(generateUrl('apps/files'))
+ .then(() => {
+ if (response.data.update_required) {
+ showInfo(
+ t('settings', 'The app has been enabled but needs to be updated.'),
+ {
+ onClick: () => window.location.reload(),
+ close: false,
+ },
+ )
+ setTimeout(() => {
+ location.reload()
+ }, 5000)
+ }
+ })
+ .catch(() => {
+ this.setError(appId, t('settings', 'Error: This app cannot be enabled because it makes the server unstable'))
+ })
+ })
+ .catch((error) => {
+ this.setLoading(appId, false)
+ this.setLoading('install', false)
+ this.setError(appId, error.response.data.data.message)
+ this.appsApiFailure({ appId, error })
+ })
+ }).catch(() => {
+ this.setLoading(appId, false)
+ this.setLoading('install', false)
+ })
+ },
+
+ forceEnableApp(appId: string) {
+ this.setLoading(appId, true)
+ this.setLoading('install', true)
+ return confirmPassword().then(() => {
+
+ return api.post(generateUrl('/apps/app_api/apps/force'), { appId })
+ .then(() => {
+ location.reload()
+ })
+ .catch((error) => {
+ this.setLoading(appId, false)
+ this.setLoading('install', false)
+ this.setError(appId, error.response.data.data.message)
+ this.appsApiFailure({ appId, error })
+ })
+ }).catch(() => {
+ this.setLoading(appId, false)
+ this.setLoading('install', false)
+ })
+ },
+
+ disableApp(appId: string) {
+ this.setLoading(appId, true)
+ return confirmPassword().then(() => {
+
+ return api.get(generateUrl(`apps/app_api/apps/disable/${appId}`))
+ .then(() => {
+ this.setLoading(appId, false)
+ const app = this.apps.find((app) => app.id === appId)
+ if (app) {
+ app.active = false
+ if (app.removable) {
+ app.canUnInstall = true
+ }
+ }
+ return true
+ })
+ .catch((error) => {
+ this.setLoading(appId, false)
+ this.appsApiFailure({ appId, error })
+ })
+ }).catch(() => {
+ this.setLoading(appId, false)
+ })
+ },
+
+ uninstallApp(appId: string, removeData: boolean) {
+ this.setLoading(appId, true)
+ return confirmPassword().then(() => {
+
+ return api.get(generateUrl(`/apps/app_api/apps/uninstall/${appId}?removeData=${removeData}`))
+ .then(() => {
+ this.setLoading(appId, false)
+ const app = this.apps.find((app) => app.id === appId)
+ if (app) {
+ app.active = false
+ app.needsDownload = true
+ app.installed = false
+ app.canUnInstall = false
+ app.canInstall = true
+ app.daemon = null
+ app.status = {}
+ if (app.update !== null) {
+ this.updateCount--
+ }
+ app.update = undefined
+ }
+ return true
+ })
+ .catch((error) => {
+ this.setLoading(appId, false)
+ this.appsApiFailure({ appId, error })
+ })
+ })
+ },
+
+ updateApp(appId: string) {
+ this.setLoading(appId, true)
+ this.setLoading('install', true)
+ return confirmPassword().then(() => {
+
+ return api.get(generateUrl(`/apps/app_api/apps/update/${appId}`))
+ .then(() => {
+ this.setLoading(appId, false)
+ this.setLoading('install', false)
+ const app = this.apps.find((app) => app.id === appId)
+ if (app) {
+ const version = app.update
+ app.update = undefined
+ app.version = version || app.version
+ app.status = {
+ type: 'update',
+ action: 'deploy',
+ init: 0,
+ deploy: 0,
+ } as IExAppStatus
+ app.error = ''
+ }
+ this.updateCount--
+ this.updateAppsStatus()
+ return true
+ })
+ .catch((error) => {
+ this.setLoading(appId, false)
+ this.setLoading('install', false)
+ this.appsApiFailure({ appId, error })
+ })
+ }).catch(() => {
+ this.setLoading(appId, false)
+ this.setLoading('install', false)
+ })
+ },
+
+ async fetchAllApps() {
+ this.loadingList = true
+ try {
+ const response = await api.get(generateUrl('/apps/app_api/apps/list'))
+ this.apps = response.data.apps
+ this.loadingList = false
+ return true
+ } catch (error) {
+ logger.error(error as string)
+ showError(t('settings', 'An error occurred during the request. Unable to proceed.'))
+ this.loadingList = false
+ }
+ },
+
+ async fetchAppStatus(appId: string) {
+ return api.get(generateUrl(`/apps/app_api/apps/status/${appId}`))
+ .then((response) => {
+ const app = this.apps.find((app) => app.id === appId)
+ if (app) {
+ app.status = response.data
+ }
+ const initializingOrDeployingApps = this.getInitializingOrDeployingApps
+ console.debug('initializingOrDeployingApps after setAppStatus', initializingOrDeployingApps)
+ if (initializingOrDeployingApps.length === 0) {
+ console.debug('clearing interval')
+ clearInterval(this.statusUpdater as number)
+ this.statusUpdater = null
+ }
+ if (Object.hasOwn(response.data, 'error')
+ && response.data.error !== ''
+ && initializingOrDeployingApps.length === 1) {
+ clearInterval(this.statusUpdater as number)
+ this.statusUpdater = null
+ }
+ })
+ .catch((error) => {
+ this.appsApiFailure({ appId, error })
+ this.apps = this.apps.filter((app) => app.id !== appId)
+ this.updateAppsStatus()
+ })
+ },
+
+ async fetchDockerDaemons() {
+ try {
+ const { data } = await axios.get(generateUrl('/apps/app_api/daemons'))
+ this.defaultDaemon = data.daemons.find((daemon: IDeployDaemon) => daemon.name === data.default_daemon_config)
+ this.dockerDaemons = data.daemons.filter((daemon: IDeployDaemon) => daemon.accepts_deploy_id === 'docker-install')
+ } catch (error) {
+ logger.error('[app-api-store] Failed to fetch Docker daemons', { error })
+ return false
+ }
+ return true
+ },
+
+ updateAppsStatus() {
+ clearInterval(this.statusUpdater as number)
+ const initializingOrDeployingApps = this.getInitializingOrDeployingApps
+ if (initializingOrDeployingApps.length === 0) {
+ return
+ }
+ this.statusUpdater = setInterval(() => {
+ const initializingOrDeployingApps = this.getInitializingOrDeployingApps
+ console.debug('initializingOrDeployingApps', initializingOrDeployingApps)
+ initializingOrDeployingApps.forEach(app => {
+ this.fetchAppStatus(app.id)
+ })
+ }, 2000) as unknown as number
+ },
+ },
+})
diff --git a/apps/settings/src/store/apps-store.ts b/apps/settings/src/store/apps-store.ts
new file mode 100644
index 00000000000..eaf8a0d7f86
--- /dev/null
+++ b/apps/settings/src/store/apps-store.ts
@@ -0,0 +1,87 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import type { IAppstoreApp, IAppstoreCategory } from '../app-types.ts'
+
+import { showError } from '@nextcloud/dialogs'
+import { loadState } from '@nextcloud/initial-state'
+import { translate as t } from '@nextcloud/l10n'
+import { generateUrl } from '@nextcloud/router'
+import { defineStore } from 'pinia'
+
+import axios from '@nextcloud/axios'
+
+import logger from '../logger'
+import APPSTORE_CATEGORY_ICONS from '../constants/AppstoreCategoryIcons.ts'
+
+const showApiError = () => showError(t('settings', 'An error occurred during the request. Unable to proceed.'))
+
+export const useAppsStore = defineStore('settings-apps', {
+ state: () => ({
+ apps: [] as IAppstoreApp[],
+ categories: [] as IAppstoreCategory[],
+ updateCount: loadState<number>('settings', 'appstoreUpdateCount', 0),
+ loading: {
+ apps: false,
+ categories: false,
+ },
+ loadingList: false,
+ gettingCategoriesPromise: null,
+ }),
+
+ actions: {
+ async loadCategories(force = false) {
+ if (this.categories.length > 0 && !force) {
+ return
+ }
+
+ try {
+ this.loading.categories = true
+ const { data: categories } = await axios.get<IAppstoreCategory[]>(generateUrl('settings/apps/categories'))
+
+ for (const category of categories) {
+ category.icon = APPSTORE_CATEGORY_ICONS[category.id] ?? ''
+ }
+
+ this.$patch({
+ categories,
+ })
+ } catch (error) {
+ logger.error(error as Error)
+ showApiError()
+ } finally {
+ this.loading.categories = false
+ }
+ },
+
+ async loadApps(force = false) {
+ if (this.apps.length > 0 && !force) {
+ return
+ }
+
+ try {
+ this.loading.apps = true
+ const { data } = await axios.get<{ apps: IAppstoreApp[] }>(generateUrl('settings/apps/list'))
+
+ this.$patch({
+ apps: data.apps,
+ })
+ } catch (error) {
+ logger.error(error as Error)
+ showApiError()
+ } finally {
+ this.loading.apps = false
+ }
+ },
+
+ getCategoryById(categoryId: string) {
+ return this.categories.find(({ id }) => id === categoryId) ?? null
+ },
+
+ getAppById(appId: string): IAppstoreApp|null {
+ return this.apps.find(({ id }) => id === appId) ?? null
+ },
+ },
+})
diff --git a/apps/settings/src/store/apps.js b/apps/settings/src/store/apps.js
index db174df2526..e0068d3892e 100644
--- a/apps/settings/src/store/apps.js
+++ b/apps/settings/src/store/apps.js
@@ -1,41 +1,29 @@
-/*
- * @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
- *
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * 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 api from './api.js'
import Vue from 'vue'
+import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
+import { showError, showInfo } from '@nextcloud/dialogs'
+import { loadState } from '@nextcloud/initial-state'
const state = {
apps: [],
+ bundles: loadState('settings', 'appstoreBundles', []),
categories: [],
- updateCount: 0,
+ updateCount: loadState('settings', 'appstoreUpdateCount', 0),
loading: {},
- loadingList: false,
+ gettingCategoriesPromise: null,
+ appApiEnabled: loadState('settings', 'appApiEnabled', false),
}
const mutations = {
APPS_API_FAILURE(state, error) {
- OC.Notification.showHtml(t('settings', 'An error occured during the request. Unable to proceed.') + '<br>' + error.error.response.data.data.message, { timeout: 7 })
+ showError(t('settings', 'An error occurred during the request. Unable to proceed.') + '<br>' + error.error.response.data.data.message, { isHTML: true })
console.error(state, error)
},
@@ -44,6 +32,10 @@ const mutations = {
state.updateCount = updateCount
},
+ updateCategories(state, categoriesPromise) {
+ state.gettingCategoriesPromise = categoriesPromise
+ },
+
setUpdateCount(state, updateCount) {
state.updateCount = updateCount
},
@@ -80,6 +72,16 @@ const mutations = {
const app = state.apps.find(app => app.id === appId)
app.active = true
app.groups = groups
+ if (app.id === 'app_api') {
+ state.appApiEnabled = true
+ }
+ },
+
+ setInstallState(state, { appId, canInstall }) {
+ const app = state.apps.find(app => app.id === appId)
+ if (app) {
+ app.canInstall = canInstall === true
+ }
},
disableApp(state, appId) {
@@ -89,6 +91,9 @@ const mutations = {
if (app.removable) {
app.canUnInstall = true
}
+ if (app.id === 'app_api') {
+ state.appApiEnabled = false
+ }
},
uninstallApp(state, appId) {
@@ -98,6 +103,9 @@ const mutations = {
state.apps.find(app => app.id === appId).installed = false
state.apps.find(app => app.id === appId).canUnInstall = false
state.apps.find(app => app.id === appId).canInstall = true
+ if (appId === 'app_api') {
+ state.appApiEnabled = false
+ }
},
updateApp(state, appId) {
@@ -138,6 +146,9 @@ const mutations = {
}
const getters = {
+ isAppApiEnabled(state) {
+ return state.appApiEnabled
+ },
loading(state) {
return function(id) {
return state.loading[id]
@@ -149,9 +160,15 @@ const getters = {
getAllApps(state) {
return state.apps
},
+ getAppBundles(state) {
+ return state.bundles
+ },
getUpdateCount(state) {
return state.updateCount
},
+ getCategoryById: (state) => (selectedCategoryId) => {
+ return state.categories.find((category) => category.id === selectedCategoryId)
+ },
}
const actions = {
@@ -175,19 +192,19 @@ const actions = {
})
// check for server health
- return api.get(generateUrl('apps/files'))
+ return axios.get(generateUrl('apps/files/'))
.then(() => {
if (response.data.update_required) {
- OC.dialogs.info(
+ showInfo(
t(
'settings',
- 'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.'
+ 'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.',
),
- t('settings', 'App update'),
- function() {
- window.location.reload()
+ {
+ onClick: () => window.location.reload(),
+ close: false,
+
},
- true
)
setTimeout(function() {
location.reload()
@@ -196,10 +213,12 @@ const actions = {
})
.catch(() => {
if (!Array.isArray(appId)) {
+ showError(t('settings', 'Error: This app cannot be enabled because it makes the server unstable'))
context.commit('setError', {
appId: apps,
- error: t('settings', 'Error: This app can not be enabled because it makes the server unstable'),
+ error: t('settings', 'Error: This app cannot be enabled because it makes the server unstable'),
})
+ context.dispatch('disableApp', { appId })
}
})
})
@@ -226,8 +245,7 @@ const actions = {
context.commit('startLoading', 'install')
return api.post(generateUrl('settings/apps/force'), { appId })
.then((response) => {
- // TODO: find a cleaner solution
- location.reload()
+ context.commit('setInstallState', { appId, canInstall: true })
})
.catch((error) => {
context.commit('stopLoading', apps)
@@ -238,6 +256,10 @@ const actions = {
})
context.commit('APPS_API_FAILURE', { appId, error })
})
+ .finally(() => {
+ context.commit('stopLoading', apps)
+ context.commit('stopLoading', 'install')
+ })
}).catch((error) => context.commit('API_FAILURE', { appId, error }))
},
disableApp(context, { appId }) {
@@ -309,18 +331,25 @@ const actions = {
.catch((error) => context.commit('API_FAILURE', error))
},
- getCategories(context) {
- context.commit('startLoading', 'categories')
- return api.get(generateUrl('settings/apps/categories'))
- .then((response) => {
- if (response.data.length > 0) {
- context.commit('appendCategories', response.data)
+ async getCategories(context, { shouldRefetchCategories = false } = {}) {
+ if (shouldRefetchCategories || !context.state.gettingCategoriesPromise) {
+ context.commit('startLoading', 'categories')
+ try {
+ const categoriesPromise = api.get(generateUrl('settings/apps/categories'))
+ context.commit('updateCategories', categoriesPromise)
+ const categoriesPromiseResponse = await categoriesPromise
+ if (categoriesPromiseResponse.data.length > 0) {
+ context.commit('appendCategories', categoriesPromiseResponse.data)
context.commit('stopLoading', 'categories')
return true
}
+ context.commit('stopLoading', 'categories')
return false
- })
- .catch((error) => context.commit('API_FAILURE', error))
+ } catch (error) {
+ context.commit('API_FAILURE', error)
+ }
+ }
+ return context.state.gettingCategoriesPromise
},
}
diff --git a/apps/settings/src/store/authtoken.ts b/apps/settings/src/store/authtoken.ts
new file mode 100644
index 00000000000..daf5583ab8c
--- /dev/null
+++ b/apps/settings/src/store/authtoken.ts
@@ -0,0 +1,200 @@
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import { showError } from '@nextcloud/dialogs'
+import { loadState } from '@nextcloud/initial-state'
+import { translate as t } from '@nextcloud/l10n'
+import { confirmPassword } from '@nextcloud/password-confirmation'
+import { generateUrl } from '@nextcloud/router'
+import { defineStore } from 'pinia'
+
+import axios from '@nextcloud/axios'
+import logger from '../logger'
+
+import '@nextcloud/password-confirmation/dist/style.css'
+
+const BASE_URL = generateUrl('/settings/personal/authtokens')
+
+const confirm = () => {
+ return new Promise(resolve => {
+ window.OC.dialogs.confirm(
+ t('settings', 'Do you really want to wipe your data from this device?'),
+ t('settings', 'Confirm wipe'),
+ resolve,
+ true,
+ )
+ })
+}
+
+export enum TokenType {
+ TEMPORARY_TOKEN = 0,
+ PERMANENT_TOKEN = 1,
+ WIPING_TOKEN = 2,
+}
+
+export interface IToken {
+ id: number
+ canDelete: boolean
+ canRename: boolean
+ current?: true
+ /**
+ * Last activity as UNIX timestamp (in seconds)
+ */
+ lastActivity: number
+ name: string
+ type: TokenType
+ scope: Record<string, boolean>
+}
+
+export interface ITokenResponse {
+ /**
+ * The device token created
+ */
+ deviceToken: IToken
+ /**
+ * User who is assigned with this token
+ */
+ loginName: string
+ /**
+ * The token for authentication
+ */
+ token: string
+}
+
+export const useAuthTokenStore = defineStore('auth-token', {
+ state() {
+ return {
+ tokens: loadState<IToken[]>('settings', 'app_tokens', []),
+ }
+ },
+ actions: {
+ /**
+ * Update a token on server
+ * @param token Token to update
+ */
+ async updateToken(token: IToken) {
+ const { data } = await axios.put(`${BASE_URL}/${token.id}`, token)
+ return data
+ },
+
+ /**
+ * Add a new token
+ * @param name The token name
+ */
+ async addToken(name: string) {
+ logger.debug('Creating a new app token')
+
+ try {
+ await confirmPassword()
+
+ const { data } = await axios.post<ITokenResponse>(BASE_URL, { name })
+ this.tokens.push(data.deviceToken)
+ logger.debug('App token created')
+ return data
+ } catch (error) {
+ return null
+ }
+ },
+
+ /**
+ * Delete a given app token
+ * @param token Token to delete
+ */
+ async deleteToken(token: IToken) {
+ logger.debug('Deleting app token', { token })
+
+ this.tokens = this.tokens.filter(({ id }) => id !== token.id)
+
+ try {
+ await axios.delete(`${BASE_URL}/${token.id}`)
+ logger.debug('App token deleted')
+ return true
+ } catch (error) {
+ logger.error('Could not delete app token', { error })
+ showError(t('settings', 'Could not delete the app token'))
+ // Restore
+ this.tokens.push(token)
+ }
+ return false
+ },
+
+ /**
+ * Wipe a token and the connected device
+ * @param token Token to wipe
+ */
+ async wipeToken(token: IToken) {
+ logger.debug('Wiping app token', { token })
+
+ try {
+ await confirmPassword()
+
+ if (!(await confirm())) {
+ logger.debug('Wipe aborted by user')
+ return
+ }
+
+ await axios.post(`${BASE_URL}/wipe/${token.id}`)
+ logger.debug('App token marked for wipe', { token })
+
+ token.type = TokenType.WIPING_TOKEN
+ token.canRename = false // wipe tokens can not be renamed
+ return true
+ } catch (error) {
+ logger.error('Could not wipe app token', { error })
+ showError(t('settings', 'Error while wiping the device with the token'))
+ }
+ return false
+ },
+
+ /**
+ * Rename an existing token
+ * @param token The token to rename
+ * @param newName The new name to set
+ */
+ async renameToken(token: IToken, newName: string) {
+ logger.debug(`renaming app token ${token.id} from ${token.name} to '${newName}'`)
+
+ const oldName = token.name
+ token.name = newName
+
+ try {
+ await this.updateToken(token)
+ logger.debug('App token name updated')
+ return true
+ } catch (error) {
+ logger.error('Could not update app token name', { error })
+ showError(t('settings', 'Error while updating device token name'))
+ // Restore
+ token.name = oldName
+ }
+ return false
+ },
+
+ /**
+ * Set scope of the token
+ * @param token Token to set scope
+ * @param scope scope to set
+ * @param value value to set
+ */
+ async setTokenScope(token: IToken, scope: string, value: boolean) {
+ logger.debug('Updating app token scope', { token, scope, value })
+
+ const oldVal = token.scope[scope]
+ token.scope[scope] = value
+
+ try {
+ await this.updateToken(token)
+ logger.debug('app token scope updated')
+ return true
+ } catch (error) {
+ logger.error('could not update app token scope', { error })
+ showError(t('settings', 'Error while updating device token scope'))
+ // Restore
+ token.scope[scope] = oldVal
+ }
+ return false
+ },
+ },
+
+})
diff --git a/apps/settings/src/store/index.js b/apps/settings/src/store/index.js
index 9f4313413eb..9ecda7e37ad 100644
--- a/apps/settings/src/store/index.js
+++ b/apps/settings/src/store/index.js
@@ -1,34 +1,14 @@
/**
- * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Julius Härtl <jus@bitgrid.net>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * 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 Vue from 'vue'
-import Vuex from 'vuex'
-import users from './users'
-import apps from './apps'
-import settings from './settings'
-import oc from './oc'
-
-Vue.use(Vuex)
+import { Store } from 'vuex'
+import users from './users.js'
+import apps from './apps.js'
+import settings from './users-settings.js'
+import oc from './oc.js'
+import { showError } from '@nextcloud/dialogs'
const debug = process.env.NODE_ENV !== 'production'
@@ -36,22 +16,28 @@ const mutations = {
API_FAILURE(state, error) {
try {
const message = error.error.response.data.ocs.meta.message
- OC.Notification.showHtml(t('settings', 'An error occured during the request. Unable to proceed.') + '<br>' + message, { timeout: 7 })
+ showError(t('settings', 'An error occurred during the request. Unable to proceed.') + '<br>' + message, { isHTML: true })
} catch (e) {
- OC.Notification.showTemporary(t('settings', 'An error occured during the request. Unable to proceed.'))
+ showError(t('settings', 'An error occurred during the request. Unable to proceed.'))
}
console.error(state, error)
},
}
-export default new Vuex.Store({
- modules: {
- users,
- apps,
- settings,
- oc,
- },
- strict: debug,
+let store = null
- mutations,
-})
+export const useStore = () => {
+ if (store === null) {
+ store = new Store({
+ modules: {
+ users,
+ apps,
+ settings,
+ oc,
+ },
+ strict: debug,
+ mutations,
+ })
+ }
+ return store
+}
diff --git a/apps/settings/src/store/oc.js b/apps/settings/src/store/oc.js
index 9e7a4e6655b..cb9b8782bce 100644
--- a/apps/settings/src/store/oc.js
+++ b/apps/settings/src/store/oc.js
@@ -1,26 +1,9 @@
/**
- * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * 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 api from './api.js'
import { generateOcsUrl } from '@nextcloud/router'
const state = {}
@@ -28,18 +11,18 @@ const mutations = {}
const getters = {}
const actions = {
/**
- * Set application config in database
- *
- * @param {Object} context store context
- * @param {Object} options destructuring object
+ * Set application config in database
+ *
+ * @param {object} context store context
+ * @param {object} options destructuring object
* @param {string} options.app Application name
* @param {boolean} options.key Config key
* @param {boolean} options.value Value to set
- * @returns{Promise}
+ * @return {Promise}
*/
setAppConfig(context, { app, key, value }) {
return api.requireAdmin().then((response) => {
- return api.post(generateOcsUrl(`apps/provisioning_api/api/v1/config/apps/${app}/${key}`, 2), { value })
+ return api.post(generateOcsUrl('apps/provisioning_api/api/v1/config/apps/{app}/{key}', { app, key }), { value })
.catch((error) => { throw error })
}).catch((error) => context.commit('API_FAILURE', { app, key, value, error }))
},
diff --git a/apps/settings/src/store/settings.js b/apps/settings/src/store/settings.js
deleted file mode 100644
index cab7a811b33..00000000000
--- a/apps/settings/src/store/settings.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * 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/>.
- *
- */
-
-const state = {
- serverData: {},
-}
-const mutations = {
- setServerData(state, data) {
- state.serverData = data
- },
-}
-const getters = {
- getServerData(state) {
- return state.serverData
- },
-}
-const actions = {}
-
-export default { state, mutations, getters, actions }
diff --git a/apps/settings/src/store/users-settings.js b/apps/settings/src/store/users-settings.js
new file mode 100644
index 00000000000..6e52b8ec0f5
--- /dev/null
+++ b/apps/settings/src/store/users-settings.js
@@ -0,0 +1,23 @@
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import { loadState } from '@nextcloud/initial-state'
+
+const state = {
+ serverData: loadState('settings', 'usersSettings', {}),
+}
+const mutations = {
+ setServerData(state, data) {
+ state.serverData = data
+ },
+}
+const getters = {
+ getServerData(state) {
+ return state.serverData
+ },
+}
+const actions = {}
+
+export default { state, mutations, getters, actions }
diff --git a/apps/settings/src/store/users.js b/apps/settings/src/store/users.js
index 7877c3f3a52..7e4b9c4aebb 100644
--- a/apps/settings/src/store/users.js
+++ b/apps/settings/src/store/users.js
@@ -1,42 +1,29 @@
/**
- * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- *
- * @license GNU AGPL version 3 or any later version
- *
- * 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'
-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: '',
@@ -49,47 +36,67 @@ 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)
}
},
+ 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) {
@@ -105,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)
@@ -116,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
@@ -128,27 +133,85 @@ const mutations = {
},
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) {
- state.users.push(response.data.ocs.data)
+ 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
- // increment or not
- if (state.userCount > 0) {
- state.groups.find(group => group.id === 'disabled').usercount += enabled ? -1 : 1
- state.userCount += enabled ? 1 : -1
- user.groups.forEach(group => {
- // Increment disabled count
- state.groups.find(groupSearch => groupSearch.id === group).disabled += enabled ? -1 : 1
+ 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 = 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
@@ -157,11 +220,47 @@ const mutations = {
/**
* Reset users list
- * @param {Object} state the store state
+ *
+ * @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)
+ })
},
}
@@ -172,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
@@ -185,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
@@ -196,15 +319,50 @@ 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 {int} options.offset List offset to request
- * @param {int} options.limit List number to return from offset
+ * @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
- * @returns {Promise}
+ * @return {Promise}
*/
getUsers(context, { offset, limit, search, group }) {
if (searchRequestCancelSource) {
@@ -212,17 +370,25 @@ 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/${encodeURIComponent(encodeURIComponent(group))}/users/details?offset=${offset}&limit=${limit}&search=${search}`, 2), {
+ 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) => {
- if (Object.keys(response.data.ocs.data.users).length > 0) {
+ const usersCount = Object.keys(response.data.ocs.data.users).length
+ if (usersCount > 0) {
context.commit('appendUsers', response.data.ocs.data.users)
- return Object.keys(response.data.ocs.data.users).length >= limit
}
- return false
+ return usersCount
})
.catch((error) => {
if (!axios.isCancel(error)) {
@@ -231,15 +397,15 @@ const actions = {
})
}
- return api.get(generateOcsUrl(`cloud/users/details?offset=${offset}&limit=${limit}&search=${search}`, 2), {
+ return api.get(generateOcsUrl('cloud/users/details?offset={offset}&limit={limit}&search={search}', { offset, limit, search }), {
cancelToken: searchRequestCancelSource.token,
})
.then((response) => {
- if (Object.keys(response.data.ocs.data.users).length > 0) {
+ const usersCount = Object.keys(response.data.ocs.data.users).length
+ if (usersCount > 0) {
context.commit('appendUsers', response.data.ocs.data.users)
- return Object.keys(response.data.ocs.data.users).length >= limit
}
- return false
+ return usersCount
})
.catch((error) => {
if (!axios.isCancel(error)) {
@@ -248,14 +414,63 @@ 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}`
- return api.get(generateOcsUrl(`cloud/groups?offset=${offset}&search=${search}${limitParam}`, 2))
+ 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', { gid: group, displayName: group })
+ context.commit('addGroup', { id: group, name: group })
})
return true
}
@@ -267,15 +482,16 @@ const actions = {
/**
* Get all users with full details
*
- * @param {Object} context store context
- * @param {Object} options destructuring object
- * @param {int} options.offset List offset to request
- * @param {int} options.limit List number to return from offset
- * @returns {Promise}
+ * @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}`, 2))
+ 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)
@@ -289,22 +505,23 @@ const actions = {
/**
* Get all users with full details from a groupid
*
- * @param {Object} context store context
- * @param {Object} options destructuring object
- * @param {int} options.offset List offset to request
- * @param {int} options.limit List number to return from offset
- * @returns {Promise}
+ * @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/${encodeURIComponent(encodeURIComponent(groupid))}/details?offset=${offset}&limit=${limit}`, 2))
+ 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 (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
},
@@ -312,15 +529,15 @@ const actions = {
/**
* Add group
*
- * @param {Object} context store context
+ * @param {object} context store context
* @param {string} gid Group id
- * @returns {Promise}
+ * @return {Promise}
*/
addGroup(context, gid) {
return api.requireAdmin().then((response) => {
- return api.post(generateOcsUrl('cloud/groups', 2), { groupid: gid })
+ 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 })
@@ -333,15 +550,39 @@ const actions = {
},
/**
+ * 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 {object} context store context
* @param {string} gid Group id
- * @returns {Promise}
+ * @return {Promise}
*/
removeGroup(context, gid) {
return api.requireAdmin().then((response) => {
- return api.delete(generateOcsUrl(`cloud/groups/${encodeURIComponent(encodeURIComponent(gid))}`, 2))
+ 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 }))
@@ -350,15 +591,15 @@ const actions = {
/**
* Add user to group
*
- * @param {Object} context store context
- * @param {Object} options destructuring object
+ * @param {object} context store context
+ * @param {object} options destructuring object
* @param {string} options.userid User id
* @param {string} options.gid Group id
- * @returns {Promise}
+ * @return {Promise}
*/
addUserGroup(context, { userid, gid }) {
return api.requireAdmin().then((response) => {
- return api.post(generateOcsUrl(`cloud/users/${userid}/groups`, 2), { groupid: gid })
+ 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 }))
@@ -367,15 +608,15 @@ const actions = {
/**
* Remove user from group
*
- * @param {Object} context store context
- * @param {Object} options destructuring object
+ * @param {object} context store context
+ * @param {object} options destructuring object
* @param {string} options.userid User id
* @param {string} options.gid Group id
- * @returns {Promise}
+ * @return {Promise}
*/
removeUserGroup(context, { userid, gid }) {
return api.requireAdmin().then((response) => {
- return api.delete(generateOcsUrl(`cloud/users/${userid}/groups`, 2), { groupid: gid })
+ return api.delete(generateOcsUrl('cloud/users/{userid}/groups', { userid }), { groupid: gid })
.then((response) => context.commit('removeUserGroup', { userid, gid }))
.catch((error) => { throw error })
}).catch((error) => {
@@ -389,15 +630,15 @@ const actions = {
/**
* Add user to group admin
*
- * @param {Object} context store context
- * @param {Object} options destructuring object
+ * @param {object} context store context
+ * @param {object} options destructuring object
* @param {string} options.userid User id
* @param {string} options.gid Group id
- * @returns {Promise}
+ * @return {Promise}
*/
addUserSubAdmin(context, { userid, gid }) {
return api.requireAdmin().then((response) => {
- return api.post(generateOcsUrl(`cloud/users/${userid}/subadmins`, 2), { groupid: gid })
+ 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 }))
@@ -406,15 +647,15 @@ const actions = {
/**
* Remove user from group admin
*
- * @param {Object} context store context
- * @param {Object} options destructuring object
+ * @param {object} context store context
+ * @param {object} options destructuring object
* @param {string} options.userid User id
* @param {string} options.gid Group id
- * @returns {Promise}
+ * @return {Promise}
*/
removeUserSubAdmin(context, { userid, gid }) {
return api.requireAdmin().then((response) => {
- return api.delete(generateOcsUrl(`cloud/users/${userid}/subadmins`, 2), { groupid: gid })
+ 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 }))
@@ -423,27 +664,30 @@ const actions = {
/**
* Mark all user devices for remote wipe
*
- * @param {Object} context store context
+ * @param {object} context store context
* @param {string} userid User id
- * @returns {Promise}
+ * @return {Promise}
*/
- wipeUserDevices(context, userid) {
- return api.requireAdmin().then((response) => {
- return api.post(generateOcsUrl(`cloud/users/${userid}/wipe`, 2))
- .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'))
+ }
},
/**
* Delete a user
*
- * @param {Object} context store context
+ * @param {object} context store context
* @param {string} userid User id
- * @returns {Promise}
+ * @return {Promise}
*/
deleteUser(context, userid) {
return api.requireAdmin().then((response) => {
- return api.delete(generateOcsUrl(`cloud/users/${userid}`, 2))
+ 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 }))
@@ -452,8 +696,10 @@ const actions = {
/**
* Add a user
*
- * @param {Object} context store context
- * @param {Object} options destructuring object
+ * @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
@@ -461,11 +707,13 @@ const actions = {
* @param {string} options.groups User groups
* @param {string} options.subadmin User subadmin groups
* @param {string} options.quota User email
- * @returns {Promise}
+ * @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', 2), { 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) => {
@@ -477,30 +725,31 @@ const actions = {
/**
* Get user data and commit addition
*
- * @param {Object} context store context
+ * @param {object} context store context
* @param {string} userid User id
- * @returns {Promise}
+ * @return {Promise}
*/
addUserData(context, userid) {
return api.requireAdmin().then((response) => {
- return api.get(generateOcsUrl(`cloud/users/${userid}`, 2))
+ 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
+ /**
+ * Enable or disable user
*
- * @param {Object} context store context
- * @param {Object} options destructuring object
+ * @param {object} context store context
+ * @param {object} options destructuring object
* @param {string} options.userid User id
* @param {boolean} options.enabled User enablement status
- * @returns {Promise}
+ * @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}`, 2))
+ 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 }))
@@ -509,43 +758,46 @@ const actions = {
/**
* Edit user data
*
- * @param {Object} context store context
- * @param {Object} options destructuring object
+ * @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
- * @returns {Promise}
+ * @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}`, 2), { 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'))
},
/**
* Send welcome mail
*
- * @param {Object} context store context
+ * @param {object} context store context
* @param {string} userid User id
- * @returns {Promise}
+ * @return {Promise}
*/
sendWelcomeMail(context, userid) {
return api.requireAdmin().then((response) => {
- return api.post(generateOcsUrl(`cloud/users/${userid}/welcome`, 2))
+ return api.post(generateOcsUrl('cloud/users/{userid}/welcome', { userid }))
.then(response => true)
.catch((error) => { throw error })
}).catch((error) => context.commit('API_FAILURE', { userid, error }))