123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553 |
- <!--
- - @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/>.
- -
- -->
-
- <template>
- <div id="app-content" class="user-list-grid" @scroll.passive="onScroll">
- <div id="grid-header" class="row" :class="{'sticky': scrolled && !showConfig.showNewUserForm}">
- <div id="headerAvatar" class="avatar" />
- <div id="headerName" class="name">
- {{ t('settings', 'Username') }}
- </div>
- <div id="headerDisplayName" class="displayName">
- {{ t('settings', 'Display name') }}
- </div>
- <div id="headerPassword" class="password">
- {{ t('settings', 'Password') }}
- </div>
- <div id="headerAddress" class="mailAddress">
- {{ t('settings', 'Email') }}
- </div>
- <div id="headerGroups" class="groups">
- {{ t('settings', 'Groups') }}
- </div>
- <div v-if="subAdminsGroups.length>0 && settings.isAdmin"
- id="headerSubAdmins"
- class="subadmins">
- {{ t('settings', 'Group admin for') }}
- </div>
- <div id="headerQuota" class="quota">
- {{ t('settings', 'Quota') }}
- </div>
- <div v-if="showConfig.showLanguages"
- id="headerLanguages"
- class="languages">
- {{ t('settings', 'Language') }}
- </div>
- <div v-if="showConfig.showStoragePath"
- class="headerStorageLocation storageLocation">
- {{ t('settings', 'Storage location') }}
- </div>
- <div v-if="showConfig.showUserBackend"
- class="headerUserBackend userBackend">
- {{ t('settings', 'User backend') }}
- </div>
- <div v-if="showConfig.showLastLogin"
- class="headerLastLogin lastLogin">
- {{ t('settings', 'Last login') }}
- </div>
- <div class="userActions" />
- </div>
-
- <form v-show="showConfig.showNewUserForm"
- id="new-user"
- class="row"
- :disabled="loading.all"
- :class="{'sticky': scrolled && showConfig.showNewUserForm}"
- @submit.prevent="createUser">
- <div :class="loading.all?'icon-loading-small':'icon-add'" />
- <div class="name">
- <input id="newusername"
- ref="newusername"
- v-model="newUser.id"
- type="text"
- required
- :placeholder="settings.newUserGenerateUserID
- ? t('settings', 'Will be autogenerated')
- : t('settings', 'Username')"
- name="username"
- autocomplete="off"
- autocapitalize="none"
- autocorrect="off"
- pattern="[a-zA-Z0-9 _\.@\-']+"
- :disabled="settings.newUserGenerateUserID">
- </div>
- <div class="displayName">
- <input id="newdisplayname"
- v-model="newUser.displayName"
- type="text"
- :placeholder="t('settings', 'Display name')"
- name="displayname"
- autocomplete="off"
- autocapitalize="none"
- autocorrect="off">
- </div>
- <div class="password">
- <input id="newuserpassword"
- ref="newuserpassword"
- v-model="newUser.password"
- type="password"
- :required="newUser.mailAddress===''"
- :placeholder="t('settings', 'Password')"
- name="password"
- autocomplete="new-password"
- autocapitalize="none"
- autocorrect="off"
- :minlength="minPasswordLength">
- </div>
- <div class="mailAddress">
- <input id="newemail"
- v-model="newUser.mailAddress"
- type="email"
- :required="newUser.password==='' || settings.newUserRequireEmail"
- :placeholder="t('settings', 'Email')"
- name="email"
- autocomplete="off"
- autocapitalize="none"
- autocorrect="off">
- </div>
- <div class="groups">
- <!-- hidden input trick for vanilla html5 form validation -->
- <input v-if="!settings.isAdmin"
- id="newgroups"
- type="text"
- :value="newUser.groups"
- tabindex="-1"
- :required="!settings.isAdmin"
- :class="{'icon-loading-small': loading.groups}">
- <Multiselect v-model="newUser.groups"
- :options="canAddGroups"
- :disabled="loading.groups||loading.all"
- tag-placeholder="create"
- :placeholder="t('settings', 'Add user in group')"
- label="name"
- track-by="id"
- class="multiselect-vue"
- :multiple="true"
- :taggable="true"
- :close-on-select="false"
- :tag-width="60"
- @tag="createGroup">
- <!-- If user is not admin, he is a subadmin.
- Subadmins can't create users outside their groups
- Therefore, empty select is forbidden -->
- <span slot="noResult">{{ t('settings', 'No results') }}</span>
- </Multiselect>
- </div>
- <div v-if="subAdminsGroups.length>0 && settings.isAdmin" class="subadmins">
- <Multiselect v-model="newUser.subAdminsGroups"
- :options="subAdminsGroups"
- :placeholder="t('settings', 'Set user as admin for')"
- label="name"
- track-by="id"
- class="multiselect-vue"
- :multiple="true"
- :close-on-select="false"
- :tag-width="60">
- <span slot="noResult">{{ t('settings', 'No results') }}</span>
- </Multiselect>
- </div>
- <div class="quota">
- <Multiselect v-model="newUser.quota"
- :options="quotaOptions"
- :placeholder="t('settings', 'Select user quota')"
- label="label"
- track-by="id"
- class="multiselect-vue"
- :allow-empty="false"
- :taggable="true"
- @tag="validateQuota" />
- </div>
- <div v-if="showConfig.showLanguages" class="languages">
- <Multiselect v-model="newUser.language"
- :options="languages"
- :placeholder="t('settings', 'Default language')"
- label="name"
- track-by="code"
- class="multiselect-vue"
- :allow-empty="false"
- group-values="languages"
- group-label="label" />
- </div>
- <div v-if="showConfig.showStoragePath" class="storageLocation" />
- <div v-if="showConfig.showUserBackend" class="userBackend" />
- <div v-if="showConfig.showLastLogin" class="lastLogin" />
- <div class="userActions">
- <input id="newsubmit"
- type="submit"
- class="button primary icon-checkmark-white has-tooltip"
- value=""
- :title="t('settings', 'Add a new user')">
- </div>
- </form>
-
- <user-row v-for="(user, key) in filteredUsers"
- :key="key"
- :user="user"
- :settings="settings"
- :show-config="showConfig"
- :groups="groups"
- :sub-admins-groups="subAdminsGroups"
- :quota-options="quotaOptions"
- :languages="languages"
- :external-actions="externalActions" />
- <InfiniteLoading ref="infiniteLoading" @infinite="infiniteHandler">
- <div slot="spinner">
- <div class="users-icon-loading icon-loading" />
- </div>
- <div slot="no-more">
- <div class="users-list-end" />
- </div>
- <div slot="no-results">
- <div id="emptycontent">
- <div class="icon-contacts-dark" />
- <h2>{{ t('settings', 'No users in here') }}</h2>
- </div>
- </div>
- </InfiniteLoading>
- </div>
- </template>
-
- <script>
- import userRow from './userList/UserRow'
- import { Multiselect } from 'nextcloud-vue'
- import InfiniteLoading from 'vue-infinite-loading'
- import Vue from 'vue'
-
- const unlimitedQuota = {
- id: 'none',
- label: t('settings', 'Unlimited')
- }
- const defaultQuota = {
- id: 'default',
- label: t('settings', 'Default quota')
- }
- const newUser = {
- id: '',
- displayName: '',
- password: '',
- mailAddress: '',
- groups: [],
- subAdminsGroups: [],
- quota: defaultQuota,
- language: {
- code: 'en',
- name: t('settings', 'Default language')
- }
- }
-
- export default {
- name: 'UserList',
- components: {
- userRow,
- Multiselect,
- InfiniteLoading
- },
- props: {
- users: {
- type: Array,
- default: () => []
- },
- showConfig: {
- type: Object,
- required: true
- },
- selectedGroup: {
- type: String,
- default: null
- },
- externalActions: {
- type: Array,
- default: () => []
- }
- },
- data() {
- return {
- unlimitedQuota,
- defaultQuota,
- loading: {
- all: false,
- groups: false
- },
- scrolled: false,
- searchQuery: '',
- newUser: Object.assign({}, newUser)
- }
- },
- computed: {
- settings() {
- return this.$store.getters.getServerData
- },
- filteredUsers() {
- if (this.selectedGroup === 'disabled') {
- return this.users.filter(user => user.enabled === false)
- }
- if (!this.settings.isAdmin) {
- // we don't want subadmins to edit themselves
- return this.users.filter(user => user.enabled !== false && user.id !== OC.getCurrentUser().uid)
- }
- return this.users.filter(user => user.enabled !== false)
- },
- groups() {
- // data provided php side + remove the disabled group
- return this.$store.getters.getGroups
- .filter(group => group.id !== 'disabled')
- .sort((a, b) => a.name.localeCompare(b.name))
- },
- canAddGroups() {
- // disabled if no permission to add new users to group
- return this.groups.map(group => {
- // clone object because we don't want
- // to edit the original groups
- group = Object.assign({}, group)
- group.$isDisabled = group.canAdd === false
- return group
- })
- },
- subAdminsGroups() {
- // data provided php side
- return this.$store.getters.getSubadminGroups
- },
- quotaOptions() {
- // convert the preset array into objects
- let quotaPreset = this.settings.quotaPreset.reduce((acc, cur) => acc.concat({ id: cur, label: cur }), [])
- // add default presets
- quotaPreset.unshift(this.unlimitedQuota)
- quotaPreset.unshift(this.defaultQuota)
- return quotaPreset
- },
- minPasswordLength() {
- return this.$store.getters.getPasswordPolicyMinLength
- },
- usersOffset() {
- return this.$store.getters.getUsersOffset
- },
- usersLimit() {
- return this.$store.getters.getUsersLimit
- },
- usersCount() {
- return this.users.length
- },
-
- /* LANGUAGES */
- languages() {
- return [
- {
- label: t('settings', 'Common languages'),
- languages: this.settings.languages.commonlanguages
- },
- {
- label: t('settings', 'All languages'),
- languages: this.settings.languages.languages
- }
- ]
- }
- },
- watch: {
- // watch url change and group select
- selectedGroup: function(val, old) {
- // if selected is the disabled group but it's empty
- this.redirectIfDisabled()
- this.$store.commit('resetUsers')
- this.$refs.infiniteLoading.stateChanger.reset()
- this.setNewUserDefaultGroup(val)
- },
-
- // make sure the infiniteLoading state is changed if we manually
- // add/remove data from the store
- usersCount: function(val, old) {
- // deleting the last user, reset the list
- if (val === 0 && old === 1) {
- this.$refs.infiniteLoading.stateChanger.reset()
- // adding the first user, warn the infiniteLoader that
- // the list is not empty anymore (we don't fetch the newly
- // added user as we already have all the info we need)
- } else if (val === 1 && old === 0) {
- this.$refs.infiniteLoading.stateChanger.loaded()
- }
- }
- },
- mounted() {
- if (!this.settings.canChangePassword) {
- OC.Notification.showTemporary(t('settings', 'Password change is disabled because the master key is disabled'))
- }
-
- /**
- * Reset and init new user form
- */
- this.resetForm()
-
- /**
- * Register search
- */
- this.userSearch = new OCA.Search(this.search, this.resetSearch)
-
- /**
- * If disabled group but empty, redirect
- */
- this.redirectIfDisabled()
- },
- methods: {
- onScroll(event) {
- this.scrolled = event.target.scrollTo > 0
- },
-
- /**
- * Validate quota string to make sure it's a valid human file size
- *
- * @param {string} quota Quota in readable format '5 GB'
- * @returns {Object}
- */
- validateQuota(quota) {
- // only used for new presets sent through @Tag
- let validQuota = OC.Util.computerFileSize(quota)
- if (validQuota !== null && validQuota >= 0) {
- // unify format output
- quota = OC.Util.humanFileSize(OC.Util.computerFileSize(quota))
- this.newUser.quota = { id: quota, label: quota }
- return this.newUser.quota
- }
- // Default is unlimited
- this.newUser.quota = this.quotaOptions[0]
- return this.quotaOptions[0]
- },
-
- infiniteHandler($state) {
- this.$store.dispatch('getUsers', {
- offset: this.usersOffset,
- limit: this.usersLimit,
- group: this.selectedGroup !== 'disabled' ? this.selectedGroup : '',
- search: this.searchQuery
- })
- .then((response) => { response ? $state.loaded() : $state.complete() })
- },
-
- /* SEARCH */
- search(query) {
- this.searchQuery = query
- this.$store.commit('resetUsers')
- this.$refs.infiniteLoading.stateChanger.reset()
- },
- resetSearch() {
- this.search('')
- },
-
- resetForm() {
- // revert form to original state
- this.newUser = Object.assign({}, newUser)
-
- /**
- * Init default language from server data. The use of this.settings
- * requires a computed variable, which break the v-model binding of the form,
- * this is a much easier solution than getter and setter on a computed var
- */
- if (this.settings.defaultLanguage) {
- Vue.set(this.newUser.language, 'code', this.settings.defaultLanguage)
- }
-
- /**
- * In case the user directly loaded the user list within a group
- * the watch won't be triggered. We need to initialize it.
- */
- this.setNewUserDefaultGroup(this.selectedGroup)
-
- this.loading.all = false
- },
- createUser() {
- this.loading.all = true
- this.$store.dispatch('addUser', {
- userid: this.newUser.id,
- password: this.newUser.password,
- displayName: this.newUser.displayName,
- email: this.newUser.mailAddress,
- groups: this.newUser.groups.map(group => group.id),
- subadmin: this.newUser.subAdminsGroups.map(group => group.id),
- quota: this.newUser.quota.id,
- language: this.newUser.language.code
- })
- .then(() => {
- this.resetForm()
- this.$refs.newusername.focus()
- })
- .catch((error) => {
- this.loading.all = false
- if (error.response && error.response.data && error.response.data.ocs && error.response.data.ocs.meta) {
- const statuscode = error.response.data.ocs.meta.statuscode
- if (statuscode === 102) {
- // wrong username
- this.$refs.newusername.focus()
- } else if (statuscode === 107) {
- // wrong password
- this.$refs.newuserpassword.focus()
- }
- }
- })
- },
- setNewUserDefaultGroup(value) {
- if (value && value.length > 0) {
- // setting new user default group to the current selected one
- let currentGroup = this.groups.find(group => group.id === value)
- if (currentGroup) {
- this.newUser.groups = [currentGroup]
- return
- }
- }
- // fallback, empty selected group
- this.newUser.groups = []
- },
-
- /**
- * Create a new group
- *
- * @param {string} gid Group id
- * @returns {Promise}
- */
- createGroup(gid) {
- this.loading.groups = true
- this.$store.dispatch('addGroup', gid)
- .then((group) => {
- this.newUser.groups.push(this.groups.find(group => group.id === gid))
- this.loading.groups = false
- })
- .catch(() => {
- this.loading.groups = false
- })
- return this.$store.getters.getGroups[this.groups.length]
- },
-
- /**
- * If the selected group is the disabled group but the count is 0
- * redirect to the all users page.
- * * we only check for 0 because we don't have the count on ldap
- * * and we therefore set the usercount to -1 in this specific case
- */
- redirectIfDisabled() {
- const allGroups = this.$store.getters.getGroups
- if (this.selectedGroup === 'disabled'
- && allGroups.findIndex(group => group.id === 'disabled' && group.usercount === 0) > -1) {
- // disabled group is empty, redirection to all users
- this.$router.push({ name: 'users' })
- this.$refs.infiniteLoading.stateChanger.reset()
- }
- }
- }
- }
- </script>
|