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 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  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" v-on:scroll.passive="onScroll">
  24. <div class="row" id="grid-header" :class="{'sticky': scrolled && !showConfig.showNewUserForm}">
  25. <div id="headerAvatar" class="avatar"></div>
  26. <div id="headerName" class="name">{{ t('settings', 'Username') }}</div>
  27. <div id="headerDisplayName" class="displayName">{{ t('settings', 'Display name') }}</div>
  28. <div id="headerPassword" class="password">{{ t('settings', 'Password') }}</div>
  29. <div id="headerAddress" class="mailAddress">{{ t('settings', 'Email') }}</div>
  30. <div id="headerGroups" class="groups">{{ t('settings', 'Groups') }}</div>
  31. <div id="headerSubAdmins" class="subadmins"
  32. v-if="subAdminsGroups.length>0 && settings.isAdmin">{{ t('settings', 'Group admin for') }}</div>
  33. <div id="headerQuota" class="quota">{{ t('settings', 'Quota') }}</div>
  34. <div id="headerLanguages" class="languages"
  35. v-if="showConfig.showLanguages">{{ t('settings', 'Language') }}</div>
  36. <div class="headerStorageLocation storageLocation"
  37. v-if="showConfig.showStoragePath">{{ t('settings', 'Storage location') }}</div>
  38. <div class="headerUserBackend userBackend"
  39. v-if="showConfig.showUserBackend">{{ t('settings', 'User backend') }}</div>
  40. <div class="headerLastLogin lastLogin"
  41. v-if="showConfig.showLastLogin">{{ t('settings', 'Last login') }}</div>
  42. <div class="userActions"></div>
  43. </div>
  44. <form class="row" id="new-user" v-show="showConfig.showNewUserForm"
  45. v-on:submit.prevent="createUser" :disabled="loading.all"
  46. :class="{'sticky': scrolled && showConfig.showNewUserForm}">
  47. <div :class="loading.all?'icon-loading-small':'icon-add'"></div>
  48. <div class="name">
  49. <input id="newusername" type="text" required v-model="newUser.id"
  50. :placeholder="this.settings.newUserGenerateUserID
  51. ? t('settings', 'Will be autogenerated')
  52. : t('settings', 'Username')"
  53. name="username" autocomplete="off" autocapitalize="none"
  54. autocorrect="off" ref="newusername" pattern="[a-zA-Z0-9 _\.@\-']+"
  55. :disabled="this.settings.newUserGenerateUserID">
  56. </div>
  57. <div class="displayName">
  58. <input id="newdisplayname" type="text" v-model="newUser.displayName"
  59. :placeholder="t('settings', 'Display name')" name="displayname"
  60. autocomplete="off" autocapitalize="none" autocorrect="off">
  61. </div>
  62. <div class="password">
  63. <input id="newuserpassword" type="password" v-model="newUser.password"
  64. :required="newUser.mailAddress===''" ref="newuserpassword"
  65. :placeholder="t('settings', 'Password')" name="password"
  66. autocomplete="new-password" autocapitalize="none" autocorrect="off"
  67. :minlength="minPasswordLength">
  68. </div>
  69. <div class="mailAddress">
  70. <input id="newemail" type="email" v-model="newUser.mailAddress"
  71. :required="newUser.password==='' || this.settings.newUserRequireEmail"
  72. :placeholder="t('settings', 'Email')" name="email"
  73. autocomplete="off" autocapitalize="none" autocorrect="off">
  74. </div>
  75. <div class="groups">
  76. <!-- hidden input trick for vanilla html5 form validation -->
  77. <input type="text" :value="newUser.groups" v-if="!settings.isAdmin"
  78. tabindex="-1" id="newgroups" :required="!settings.isAdmin"
  79. :class="{'icon-loading-small': loading.groups}"/>
  80. <multiselect v-model="newUser.groups" :options="canAddGroups" :disabled="loading.groups||loading.all"
  81. tag-placeholder="create" :placeholder="t('settings', 'Add user in group')"
  82. label="name" track-by="id" class="multiselect-vue"
  83. :multiple="true" :taggable="true" :close-on-select="false"
  84. :tag-width="60" @tag="createGroup">
  85. <!-- If user is not admin, he is a subadmin.
  86. Subadmins can't create users outside their groups
  87. Therefore, empty select is forbidden -->
  88. <span slot="noResult">{{t('settings', 'No results')}}</span>
  89. </multiselect>
  90. </div>
  91. <div class="subadmins" v-if="subAdminsGroups.length>0 && settings.isAdmin">
  92. <multiselect :options="subAdminsGroups" v-model="newUser.subAdminsGroups"
  93. :placeholder="t('settings', 'Set user as admin for')"
  94. label="name" track-by="id" class="multiselect-vue"
  95. :multiple="true" :close-on-select="false" :tag-width="60">
  96. <span slot="noResult">{{t('settings', 'No results')}}</span>
  97. </multiselect>
  98. </div>
  99. <div class="quota">
  100. <multiselect :options="quotaOptions" v-model="newUser.quota"
  101. :placeholder="t('settings', 'Select user quota')"
  102. label="label" track-by="id" class="multiselect-vue"
  103. :allowEmpty="false" :taggable="true"
  104. @tag="validateQuota" >
  105. </multiselect>
  106. </div>
  107. <div class="languages" v-if="showConfig.showLanguages">
  108. <multiselect :options="languages" v-model="newUser.language"
  109. :placeholder="t('settings', 'Default language')"
  110. label="name" track-by="code" class="multiselect-vue"
  111. :allowEmpty="false" group-values="languages" group-label="label">
  112. </multiselect>
  113. </div>
  114. <div class="storageLocation" v-if="showConfig.showStoragePath"></div>
  115. <div class="userBackend" v-if="showConfig.showUserBackend"></div>
  116. <div class="lastLogin" v-if="showConfig.showLastLogin"></div>
  117. <div class="userActions">
  118. <input type="submit" id="newsubmit" class="button primary icon-checkmark-white has-tooltip"
  119. value="" :title="t('settings', 'Add a new user')">
  120. </div>
  121. </form>
  122. <user-row v-for="(user, key) in filteredUsers" :user="user" :key="key" :settings="settings" :showConfig="showConfig"
  123. :groups="groups" :subAdminsGroups="subAdminsGroups" :quotaOptions="quotaOptions" :languages="languages"
  124. :externalActions="externalActions" />
  125. <infinite-loading @infinite="infiniteHandler" ref="infiniteLoading">
  126. <div slot="spinner"><div class="users-icon-loading icon-loading"></div></div>
  127. <div slot="no-more"><div class="users-list-end"></div></div>
  128. <div slot="no-results">
  129. <div id="emptycontent">
  130. <div class="icon-contacts-dark"></div>
  131. <h2>{{t('settings', 'No users in here')}}</h2>
  132. </div>
  133. </div>
  134. </infinite-loading>
  135. </div>
  136. </template>
  137. <script>
  138. import userRow from './userList/userRow';
  139. import { Multiselect } from 'nextcloud-vue'
  140. import InfiniteLoading from 'vue-infinite-loading';
  141. import Vue from 'vue';
  142. const unlimitedQuota = {
  143. id: 'none',
  144. label: t('settings', 'Unlimited')
  145. }
  146. const defaultQuota = {
  147. id: 'default',
  148. label: t('settings', 'Default quota')
  149. }
  150. const newUser = {
  151. id: '',
  152. displayName: '',
  153. password: '',
  154. mailAddress: '',
  155. groups: [],
  156. subAdminsGroups: [],
  157. quota: defaultQuota,
  158. language: {
  159. code: 'en',
  160. name: t('settings', 'Default language')
  161. }
  162. }
  163. export default {
  164. name: 'userList',
  165. props: ['users', 'showConfig', 'selectedGroup', 'externalActions'],
  166. components: {
  167. userRow,
  168. Multiselect,
  169. InfiniteLoading
  170. },
  171. data() {
  172. return {
  173. unlimitedQuota,
  174. defaultQuota,
  175. loading: {
  176. all: false,
  177. groups: false
  178. },
  179. scrolled: false,
  180. searchQuery: '',
  181. newUser: Object.assign({}, newUser)
  182. };
  183. },
  184. mounted() {
  185. if (!this.settings.canChangePassword) {
  186. OC.Notification.showTemporary(t('settings', 'Password change is disabled because the master key is disabled'));
  187. }
  188. /**
  189. * Reset and init new user form
  190. */
  191. this.resetForm()
  192. /**
  193. * Register search
  194. */
  195. this.userSearch = new OCA.Search(this.search, this.resetSearch);
  196. },
  197. computed: {
  198. settings() {
  199. return this.$store.getters.getServerData;
  200. },
  201. filteredUsers() {
  202. if (this.selectedGroup === 'disabled') {
  203. let disabledUsers = this.users.filter(user => user.enabled === false);
  204. if (disabledUsers.length === 0 && this.$refs.infiniteLoading && this.$refs.infiniteLoading.isComplete) {
  205. // disabled group is empty, redirection to all users
  206. this.$router.push({ name: 'users' });
  207. this.$refs.infiniteLoading.stateChanger.reset()
  208. }
  209. return disabledUsers;
  210. }
  211. if (!this.settings.isAdmin) {
  212. // we don't want subadmins to edit themselves
  213. return this.users.filter(user => user.enabled !== false && user.id !== OC.getCurrentUser().uid);
  214. }
  215. return this.users.filter(user => user.enabled !== false);
  216. },
  217. groups() {
  218. // data provided php side + remove the disabled group
  219. return this.$store.getters.getGroups
  220. .filter(group => group.id !== 'disabled')
  221. .sort((a, b) => a.name.localeCompare(b.name));
  222. },
  223. canAddGroups() {
  224. // disabled if no permission to add new users to group
  225. return this.groups.map(group => {
  226. // clone object because we don't want
  227. // to edit the original groups
  228. group = Object.assign({}, group);
  229. group.$isDisabled = group.canAdd === false;
  230. return group;
  231. });
  232. },
  233. subAdminsGroups() {
  234. // data provided php side
  235. return this.$store.getters.getSubadminGroups;
  236. },
  237. quotaOptions() {
  238. // convert the preset array into objects
  239. let quotaPreset = this.settings.quotaPreset.reduce((acc, cur) => acc.concat({id: cur, label: cur}), []);
  240. // add default presets
  241. quotaPreset.unshift(this.unlimitedQuota);
  242. quotaPreset.unshift(this.defaultQuota);
  243. return quotaPreset;
  244. },
  245. minPasswordLength() {
  246. return this.$store.getters.getPasswordPolicyMinLength;
  247. },
  248. usersOffset() {
  249. return this.$store.getters.getUsersOffset;
  250. },
  251. usersLimit() {
  252. return this.$store.getters.getUsersLimit;
  253. },
  254. usersCount() {
  255. return this.users.length
  256. },
  257. /* LANGUAGES */
  258. languages() {
  259. return Array(
  260. {
  261. label: t('settings', 'Common languages'),
  262. languages: this.settings.languages.commonlanguages
  263. },
  264. {
  265. label: t('settings', 'All languages'),
  266. languages: this.settings.languages.languages
  267. }
  268. );
  269. }
  270. },
  271. watch: {
  272. // watch url change and group select
  273. selectedGroup: function (val, old) {
  274. this.$store.commit('resetUsers');
  275. this.$refs.infiniteLoading.stateChanger.reset()
  276. this.setNewUserDefaultGroup(val);
  277. },
  278. // make sure the infiniteLoading state is changed if we manually
  279. // add/remove data from the store
  280. usersCount: function(val, old) {
  281. // deleting the last user, reset the list
  282. if (val === 0 && old === 1) {
  283. this.$refs.infiniteLoading.stateChanger.reset()
  284. // adding the first user, warn the infiniteLoader that
  285. // the list is not empty anymore (we don't fetch the newly
  286. // added user as we already have all the info we need)
  287. } else if (val === 1 && old === 0) {
  288. this.$refs.infiniteLoading.stateChanger.loaded()
  289. }
  290. }
  291. },
  292. methods: {
  293. onScroll(event) {
  294. this.scrolled = event.target.scrollTo > 0;
  295. },
  296. /**
  297. * Validate quota string to make sure it's a valid human file size
  298. *
  299. * @param {string} quota Quota in readable format '5 GB'
  300. * @returns {Object}
  301. */
  302. validateQuota(quota) {
  303. // only used for new presets sent through @Tag
  304. let validQuota = OC.Util.computerFileSize(quota);
  305. if (validQuota !== null && validQuota >= 0) {
  306. // unify format output
  307. quota = OC.Util.humanFileSize(OC.Util.computerFileSize(quota));
  308. return this.newUser.quota = {id: quota, label: quota};
  309. }
  310. // Default is unlimited
  311. return this.newUser.quota = this.quotaOptions[0];
  312. },
  313. infiniteHandler($state) {
  314. this.$store.dispatch('getUsers', {
  315. offset: this.usersOffset,
  316. limit: this.usersLimit,
  317. group: this.selectedGroup !== 'disabled' ? this.selectedGroup : '',
  318. search: this.searchQuery
  319. })
  320. .then((response) => { response ? $state.loaded() : $state.complete() });
  321. },
  322. /* SEARCH */
  323. search(query) {
  324. this.searchQuery = query;
  325. this.$store.commit('resetUsers');
  326. this.$refs.infiniteLoading.stateChanger.reset()
  327. },
  328. resetSearch() {
  329. this.search('');
  330. },
  331. resetForm() {
  332. // revert form to original state
  333. this.newUser = Object.assign({}, newUser);
  334. /**
  335. * Init default language from server data. The use of this.settings
  336. * requires a computed variable, which break the v-model binding of the form,
  337. * this is a much easier solution than getter and setter on a computed var
  338. */
  339. if (this.settings.defaultLanguage) {
  340. Vue.set(this.newUser.language, 'code', this.settings.defaultLanguage);
  341. }
  342. /**
  343. * In case the user directly loaded the user list within a group
  344. * the watch won't be triggered. We need to initialize it.
  345. */
  346. this.setNewUserDefaultGroup(this.selectedGroup);
  347. this.loading.all = false;
  348. },
  349. createUser() {
  350. this.loading.all = true;
  351. this.$store.dispatch('addUser', {
  352. userid: this.newUser.id,
  353. password: this.newUser.password,
  354. displayName: this.newUser.displayName,
  355. email: this.newUser.mailAddress,
  356. groups: this.newUser.groups.map(group => group.id),
  357. subadmin: this.newUser.subAdminsGroups.map(group => group.id),
  358. quota: this.newUser.quota.id,
  359. language: this.newUser.language.code,
  360. })
  361. .then(() => {
  362. this.resetForm()
  363. this.$refs.newusername.focus();
  364. })
  365. .catch((error) => {
  366. this.loading.all = false;
  367. if (error.response && error.response.data && error.response.data.ocs && error.response.data.ocs.meta) {
  368. const statuscode = error.response.data.ocs.meta.statuscode
  369. if (statuscode === 102) {
  370. // wrong username
  371. this.$refs.newusername.focus();
  372. } else if (statuscode === 107) {
  373. // wrong password
  374. this.$refs.newuserpassword.focus();
  375. }
  376. }
  377. });
  378. },
  379. setNewUserDefaultGroup(value) {
  380. if (value && value.length > 0) {
  381. // setting new user default group to the current selected one
  382. let currentGroup = this.groups.find(group => group.id === value);
  383. if (currentGroup) {
  384. this.newUser.groups = [currentGroup];
  385. return;
  386. }
  387. }
  388. // fallback, empty selected group
  389. this.newUser.groups = [];
  390. },
  391. /**
  392. * Create a new group
  393. *
  394. * @param {string} groups Group id
  395. * @returns {Promise}
  396. */
  397. createGroup(gid) {
  398. this.loading.groups = true;
  399. this.$store.dispatch('addGroup', gid)
  400. .then((group) => {
  401. this.newUser.groups.push(this.groups.find(group => group.id === gid))
  402. this.loading.groups = false;
  403. })
  404. .catch(() => {
  405. this.loading.groups = false;
  406. });
  407. return this.$store.getters.getGroups[this.groups.length];
  408. }
  409. }
  410. }
  411. </script>