You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

UserList.vue 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. <!--
  2. - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
  3. -
  4. - @author John Molakvoæ <skjnldsv@protonmail.com>
  5. -
  6. - @license GNU AGPL version 3 or any later version
  7. -
  8. - This program is free software: you can redistribute it and/or modify
  9. - it under the terms of the GNU Affero General Public License as
  10. - published by the Free Software Foundation, either version 3 of the
  11. - License, or (at your option) any later version.
  12. -
  13. - This program is distributed in the hope that it will be useful,
  14. - but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. - GNU Affero General Public License for more details.
  17. -
  18. - You should have received a copy of the GNU Affero General Public License
  19. - along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. -
  21. -->
  22. <template>
  23. <div id="app-content" class="user-list-grid" @scroll.passive="onScroll">
  24. <div id="grid-header" class="row" :class="{'sticky': scrolled && !showConfig.showNewUserForm}">
  25. <div id="headerAvatar" class="avatar" />
  26. <div id="headerName" class="name">
  27. {{ t('settings', 'Username') }}
  28. </div>
  29. <div id="headerDisplayName" class="displayName">
  30. {{ t('settings', 'Display name') }}
  31. </div>
  32. <div id="headerPassword" class="password">
  33. {{ t('settings', 'Password') }}
  34. </div>
  35. <div id="headerAddress" class="mailAddress">
  36. {{ t('settings', 'Email') }}
  37. </div>
  38. <div id="headerGroups" class="groups">
  39. {{ t('settings', 'Groups') }}
  40. </div>
  41. <div v-if="subAdminsGroups.length>0 && settings.isAdmin"
  42. id="headerSubAdmins"
  43. class="subadmins">
  44. {{ t('settings', 'Group admin for') }}
  45. </div>
  46. <div id="headerQuota" class="quota">
  47. {{ t('settings', 'Quota') }}
  48. </div>
  49. <div v-if="showConfig.showLanguages"
  50. id="headerLanguages"
  51. class="languages">
  52. {{ t('settings', 'Language') }}
  53. </div>
  54. <div v-if="showConfig.showStoragePath"
  55. class="headerStorageLocation storageLocation">
  56. {{ t('settings', 'Storage location') }}
  57. </div>
  58. <div v-if="showConfig.showUserBackend"
  59. class="headerUserBackend userBackend">
  60. {{ t('settings', 'User backend') }}
  61. </div>
  62. <div v-if="showConfig.showLastLogin"
  63. class="headerLastLogin lastLogin">
  64. {{ t('settings', 'Last login') }}
  65. </div>
  66. <div class="userActions" />
  67. </div>
  68. <form v-show="showConfig.showNewUserForm"
  69. id="new-user"
  70. class="row"
  71. :disabled="loading.all"
  72. :class="{'sticky': scrolled && showConfig.showNewUserForm}"
  73. @submit.prevent="createUser">
  74. <div :class="loading.all?'icon-loading-small':'icon-add'" />
  75. <div class="name">
  76. <input id="newusername"
  77. ref="newusername"
  78. v-model="newUser.id"
  79. type="text"
  80. required
  81. :placeholder="settings.newUserGenerateUserID
  82. ? t('settings', 'Will be autogenerated')
  83. : t('settings', 'Username')"
  84. name="username"
  85. autocomplete="off"
  86. autocapitalize="none"
  87. autocorrect="off"
  88. pattern="[a-zA-Z0-9 _\.@\-']+"
  89. :disabled="settings.newUserGenerateUserID">
  90. </div>
  91. <div class="displayName">
  92. <input id="newdisplayname"
  93. v-model="newUser.displayName"
  94. type="text"
  95. :placeholder="t('settings', 'Display name')"
  96. name="displayname"
  97. autocomplete="off"
  98. autocapitalize="none"
  99. autocorrect="off">
  100. </div>
  101. <div class="password">
  102. <input id="newuserpassword"
  103. ref="newuserpassword"
  104. v-model="newUser.password"
  105. type="password"
  106. :required="newUser.mailAddress===''"
  107. :placeholder="t('settings', 'Password')"
  108. name="password"
  109. autocomplete="new-password"
  110. autocapitalize="none"
  111. autocorrect="off"
  112. :minlength="minPasswordLength">
  113. </div>
  114. <div class="mailAddress">
  115. <input id="newemail"
  116. v-model="newUser.mailAddress"
  117. type="email"
  118. :required="newUser.password==='' || settings.newUserRequireEmail"
  119. :placeholder="t('settings', 'Email')"
  120. name="email"
  121. autocomplete="off"
  122. autocapitalize="none"
  123. autocorrect="off">
  124. </div>
  125. <div class="groups">
  126. <!-- hidden input trick for vanilla html5 form validation -->
  127. <input v-if="!settings.isAdmin"
  128. id="newgroups"
  129. type="text"
  130. :value="newUser.groups"
  131. tabindex="-1"
  132. :required="!settings.isAdmin"
  133. :class="{'icon-loading-small': loading.groups}">
  134. <Multiselect v-model="newUser.groups"
  135. :options="canAddGroups"
  136. :disabled="loading.groups||loading.all"
  137. tag-placeholder="create"
  138. :placeholder="t('settings', 'Add user in group')"
  139. label="name"
  140. track-by="id"
  141. class="multiselect-vue"
  142. :multiple="true"
  143. :taggable="true"
  144. :close-on-select="false"
  145. :tag-width="60"
  146. @tag="createGroup">
  147. <!-- If user is not admin, he is a subadmin.
  148. Subadmins can't create users outside their groups
  149. Therefore, empty select is forbidden -->
  150. <span slot="noResult">{{ t('settings', 'No results') }}</span>
  151. </Multiselect>
  152. </div>
  153. <div v-if="subAdminsGroups.length>0 && settings.isAdmin" class="subadmins">
  154. <Multiselect v-model="newUser.subAdminsGroups"
  155. :options="subAdminsGroups"
  156. :placeholder="t('settings', 'Set user as admin for')"
  157. label="name"
  158. track-by="id"
  159. class="multiselect-vue"
  160. :multiple="true"
  161. :close-on-select="false"
  162. :tag-width="60">
  163. <span slot="noResult">{{ t('settings', 'No results') }}</span>
  164. </Multiselect>
  165. </div>
  166. <div class="quota">
  167. <Multiselect v-model="newUser.quota"
  168. :options="quotaOptions"
  169. :placeholder="t('settings', 'Select user quota')"
  170. label="label"
  171. track-by="id"
  172. class="multiselect-vue"
  173. :allow-empty="false"
  174. :taggable="true"
  175. @tag="validateQuota" />
  176. </div>
  177. <div v-if="showConfig.showLanguages" class="languages">
  178. <Multiselect v-model="newUser.language"
  179. :options="languages"
  180. :placeholder="t('settings', 'Default language')"
  181. label="name"
  182. track-by="code"
  183. class="multiselect-vue"
  184. :allow-empty="false"
  185. group-values="languages"
  186. group-label="label" />
  187. </div>
  188. <div v-if="showConfig.showStoragePath" class="storageLocation" />
  189. <div v-if="showConfig.showUserBackend" class="userBackend" />
  190. <div v-if="showConfig.showLastLogin" class="lastLogin" />
  191. <div class="userActions">
  192. <input id="newsubmit"
  193. type="submit"
  194. class="button primary icon-checkmark-white has-tooltip"
  195. value=""
  196. :title="t('settings', 'Add a new user')">
  197. </div>
  198. </form>
  199. <user-row v-for="(user, key) in filteredUsers"
  200. :key="key"
  201. :user="user"
  202. :settings="settings"
  203. :show-config="showConfig"
  204. :groups="groups"
  205. :sub-admins-groups="subAdminsGroups"
  206. :quota-options="quotaOptions"
  207. :languages="languages"
  208. :external-actions="externalActions" />
  209. <InfiniteLoading ref="infiniteLoading" @infinite="infiniteHandler">
  210. <div slot="spinner">
  211. <div class="users-icon-loading icon-loading" />
  212. </div>
  213. <div slot="no-more">
  214. <div class="users-list-end" />
  215. </div>
  216. <div slot="no-results">
  217. <div id="emptycontent">
  218. <div class="icon-contacts-dark" />
  219. <h2>{{ t('settings', 'No users in here') }}</h2>
  220. </div>
  221. </div>
  222. </InfiniteLoading>
  223. </div>
  224. </template>
  225. <script>
  226. import userRow from './userList/UserRow'
  227. import { Multiselect } from 'nextcloud-vue'
  228. import InfiniteLoading from 'vue-infinite-loading'
  229. import Vue from 'vue'
  230. const unlimitedQuota = {
  231. id: 'none',
  232. label: t('settings', 'Unlimited')
  233. }
  234. const defaultQuota = {
  235. id: 'default',
  236. label: t('settings', 'Default quota')
  237. }
  238. const newUser = {
  239. id: '',
  240. displayName: '',
  241. password: '',
  242. mailAddress: '',
  243. groups: [],
  244. subAdminsGroups: [],
  245. quota: defaultQuota,
  246. language: {
  247. code: 'en',
  248. name: t('settings', 'Default language')
  249. }
  250. }
  251. export default {
  252. name: 'UserList',
  253. components: {
  254. userRow,
  255. Multiselect,
  256. InfiniteLoading
  257. },
  258. props: {
  259. users: {
  260. type: Array,
  261. default: () => []
  262. },
  263. showConfig: {
  264. type: Object,
  265. required: true
  266. },
  267. selectedGroup: {
  268. type: String,
  269. default: null
  270. },
  271. externalActions: {
  272. type: Array,
  273. default: () => []
  274. }
  275. },
  276. data() {
  277. return {
  278. unlimitedQuota,
  279. defaultQuota,
  280. loading: {
  281. all: false,
  282. groups: false
  283. },
  284. scrolled: false,
  285. searchQuery: '',
  286. newUser: Object.assign({}, newUser)
  287. }
  288. },
  289. computed: {
  290. settings() {
  291. return this.$store.getters.getServerData
  292. },
  293. filteredUsers() {
  294. if (this.selectedGroup === 'disabled') {
  295. return this.users.filter(user => user.enabled === false)
  296. }
  297. if (!this.settings.isAdmin) {
  298. // we don't want subadmins to edit themselves
  299. return this.users.filter(user => user.enabled !== false && user.id !== OC.getCurrentUser().uid)
  300. }
  301. return this.users.filter(user => user.enabled !== false)
  302. },
  303. groups() {
  304. // data provided php side + remove the disabled group
  305. return this.$store.getters.getGroups
  306. .filter(group => group.id !== 'disabled')
  307. .sort((a, b) => a.name.localeCompare(b.name))
  308. },
  309. canAddGroups() {
  310. // disabled if no permission to add new users to group
  311. return this.groups.map(group => {
  312. // clone object because we don't want
  313. // to edit the original groups
  314. group = Object.assign({}, group)
  315. group.$isDisabled = group.canAdd === false
  316. return group
  317. })
  318. },
  319. subAdminsGroups() {
  320. // data provided php side
  321. return this.$store.getters.getSubadminGroups
  322. },
  323. quotaOptions() {
  324. // convert the preset array into objects
  325. let quotaPreset = this.settings.quotaPreset.reduce((acc, cur) => acc.concat({ id: cur, label: cur }), [])
  326. // add default presets
  327. quotaPreset.unshift(this.unlimitedQuota)
  328. quotaPreset.unshift(this.defaultQuota)
  329. return quotaPreset
  330. },
  331. minPasswordLength() {
  332. return this.$store.getters.getPasswordPolicyMinLength
  333. },
  334. usersOffset() {
  335. return this.$store.getters.getUsersOffset
  336. },
  337. usersLimit() {
  338. return this.$store.getters.getUsersLimit
  339. },
  340. usersCount() {
  341. return this.users.length
  342. },
  343. /* LANGUAGES */
  344. languages() {
  345. return [
  346. {
  347. label: t('settings', 'Common languages'),
  348. languages: this.settings.languages.commonlanguages
  349. },
  350. {
  351. label: t('settings', 'All languages'),
  352. languages: this.settings.languages.languages
  353. }
  354. ]
  355. }
  356. },
  357. watch: {
  358. // watch url change and group select
  359. selectedGroup: function(val, old) {
  360. // if selected is the disabled group but it's empty
  361. this.redirectIfDisabled()
  362. this.$store.commit('resetUsers')
  363. this.$refs.infiniteLoading.stateChanger.reset()
  364. this.setNewUserDefaultGroup(val)
  365. },
  366. // make sure the infiniteLoading state is changed if we manually
  367. // add/remove data from the store
  368. usersCount: function(val, old) {
  369. // deleting the last user, reset the list
  370. if (val === 0 && old === 1) {
  371. this.$refs.infiniteLoading.stateChanger.reset()
  372. // adding the first user, warn the infiniteLoader that
  373. // the list is not empty anymore (we don't fetch the newly
  374. // added user as we already have all the info we need)
  375. } else if (val === 1 && old === 0) {
  376. this.$refs.infiniteLoading.stateChanger.loaded()
  377. }
  378. }
  379. },
  380. mounted() {
  381. if (!this.settings.canChangePassword) {
  382. OC.Notification.showTemporary(t('settings', 'Password change is disabled because the master key is disabled'))
  383. }
  384. /**
  385. * Reset and init new user form
  386. */
  387. this.resetForm()
  388. /**
  389. * Register search
  390. */
  391. this.userSearch = new OCA.Search(this.search, this.resetSearch)
  392. /**
  393. * If disabled group but empty, redirect
  394. */
  395. this.redirectIfDisabled()
  396. },
  397. methods: {
  398. onScroll(event) {
  399. this.scrolled = event.target.scrollTo > 0
  400. },
  401. /**
  402. * Validate quota string to make sure it's a valid human file size
  403. *
  404. * @param {string} quota Quota in readable format '5 GB'
  405. * @returns {Object}
  406. */
  407. validateQuota(quota) {
  408. // only used for new presets sent through @Tag
  409. let validQuota = OC.Util.computerFileSize(quota)
  410. if (validQuota !== null && validQuota >= 0) {
  411. // unify format output
  412. quota = OC.Util.humanFileSize(OC.Util.computerFileSize(quota))
  413. this.newUser.quota = { id: quota, label: quota }
  414. return this.newUser.quota
  415. }
  416. // Default is unlimited
  417. this.newUser.quota = this.quotaOptions[0]
  418. return this.quotaOptions[0]
  419. },
  420. infiniteHandler($state) {
  421. this.$store.dispatch('getUsers', {
  422. offset: this.usersOffset,
  423. limit: this.usersLimit,
  424. group: this.selectedGroup !== 'disabled' ? this.selectedGroup : '',
  425. search: this.searchQuery
  426. })
  427. .then((response) => { response ? $state.loaded() : $state.complete() })
  428. },
  429. /* SEARCH */
  430. search(query) {
  431. this.searchQuery = query
  432. this.$store.commit('resetUsers')
  433. this.$refs.infiniteLoading.stateChanger.reset()
  434. },
  435. resetSearch() {
  436. this.search('')
  437. },
  438. resetForm() {
  439. // revert form to original state
  440. this.newUser = Object.assign({}, newUser)
  441. /**
  442. * Init default language from server data. The use of this.settings
  443. * requires a computed variable, which break the v-model binding of the form,
  444. * this is a much easier solution than getter and setter on a computed var
  445. */
  446. if (this.settings.defaultLanguage) {
  447. Vue.set(this.newUser.language, 'code', this.settings.defaultLanguage)
  448. }
  449. /**
  450. * In case the user directly loaded the user list within a group
  451. * the watch won't be triggered. We need to initialize it.
  452. */
  453. this.setNewUserDefaultGroup(this.selectedGroup)
  454. this.loading.all = false
  455. },
  456. createUser() {
  457. this.loading.all = true
  458. this.$store.dispatch('addUser', {
  459. userid: this.newUser.id,
  460. password: this.newUser.password,
  461. displayName: this.newUser.displayName,
  462. email: this.newUser.mailAddress,
  463. groups: this.newUser.groups.map(group => group.id),
  464. subadmin: this.newUser.subAdminsGroups.map(group => group.id),
  465. quota: this.newUser.quota.id,
  466. language: this.newUser.language.code
  467. })
  468. .then(() => {
  469. this.resetForm()
  470. this.$refs.newusername.focus()
  471. })
  472. .catch((error) => {
  473. this.loading.all = false
  474. if (error.response && error.response.data && error.response.data.ocs && error.response.data.ocs.meta) {
  475. const statuscode = error.response.data.ocs.meta.statuscode
  476. if (statuscode === 102) {
  477. // wrong username
  478. this.$refs.newusername.focus()
  479. } else if (statuscode === 107) {
  480. // wrong password
  481. this.$refs.newuserpassword.focus()
  482. }
  483. }
  484. })
  485. },
  486. setNewUserDefaultGroup(value) {
  487. if (value && value.length > 0) {
  488. // setting new user default group to the current selected one
  489. let currentGroup = this.groups.find(group => group.id === value)
  490. if (currentGroup) {
  491. this.newUser.groups = [currentGroup]
  492. return
  493. }
  494. }
  495. // fallback, empty selected group
  496. this.newUser.groups = []
  497. },
  498. /**
  499. * Create a new group
  500. *
  501. * @param {string} gid Group id
  502. * @returns {Promise}
  503. */
  504. createGroup(gid) {
  505. this.loading.groups = true
  506. this.$store.dispatch('addGroup', gid)
  507. .then((group) => {
  508. this.newUser.groups.push(this.groups.find(group => group.id === gid))
  509. this.loading.groups = false
  510. })
  511. .catch(() => {
  512. this.loading.groups = false
  513. })
  514. return this.$store.getters.getGroups[this.groups.length]
  515. },
  516. /**
  517. * If the selected group is the disabled group but the count is 0
  518. * redirect to the all users page.
  519. * * we only check for 0 because we don't have the count on ldap
  520. * * and we therefore set the usercount to -1 in this specific case
  521. */
  522. redirectIfDisabled() {
  523. const allGroups = this.$store.getters.getGroups
  524. if (this.selectedGroup === 'disabled'
  525. && allGroups.findIndex(group => group.id === 'disabled' && group.usercount === 0) > -1) {
  526. // disabled group is empty, redirection to all users
  527. this.$router.push({ name: 'users' })
  528. this.$refs.infiniteLoading.stateChanger.reset()
  529. }
  530. }
  531. }
  532. }
  533. </script>