Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

userRow.vue 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  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. <!-- Obfuscated user: Logged in user does not have permissions to see all of the data -->
  24. <div class="row" v-if="Object.keys(user).length ===1">
  25. <div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable}">
  26. <img alt="" width="32" height="32" :src="generateAvatar(user.id, 32)"
  27. :srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
  28. v-if="!loading.delete && !loading.disable">
  29. </div>
  30. <div class="name">{{user.id}}</div>
  31. <div class="obfuscated">{{t('settings','You do not have permissions to see the details of this user')}}</div>
  32. </div>
  33. <!-- User full data -->
  34. <div class="row" v-else :class="{'disabled': loading.delete || loading.disable}">
  35. <div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable}">
  36. <img alt="" width="32" height="32" :src="generateAvatar(user.id, 32)"
  37. :srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
  38. v-if="!loading.delete && !loading.disable">
  39. </div>
  40. <div class="name">{{user.id}}</div>
  41. <form class="displayName" :class="{'icon-loading-small': loading.displayName}" v-on:submit.prevent="updateDisplayName">
  42. <input :id="'displayName'+user.id+rand" type="text"
  43. :disabled="loading.displayName||loading.all"
  44. :value="user.displayname" ref="displayName"
  45. autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" />
  46. <input type="submit" class="icon-confirm" value="" />
  47. </form>
  48. <form class="password" v-if="settings.canChangePassword" :class="{'icon-loading-small': loading.password}"
  49. v-on:submit.prevent="updatePassword">
  50. <input :id="'password'+user.id+rand" type="password" required
  51. :disabled="loading.password||loading.all" :minlength="minPasswordLength"
  52. value="" :placeholder="t('settings', 'New password')" ref="password"
  53. autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" />
  54. <input type="submit" class="icon-confirm" value="" />
  55. </form>
  56. <div v-else></div>
  57. <form class="mailAddress" :class="{'icon-loading-small': loading.mailAddress}" v-on:submit.prevent="updateEmail">
  58. <input :id="'mailAddress'+user.id+rand" type="email"
  59. :disabled="loading.mailAddress||loading.all"
  60. :value="user.email" ref="mailAddress"
  61. autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" />
  62. <input type="submit" class="icon-confirm" value="" />
  63. </form>
  64. <div class="groups" :class="{'icon-loading-small': loading.groups}">
  65. <multiselect :value="userGroups" :options="groups" :disabled="loading.groups||loading.all"
  66. tag-placeholder="create" :placeholder="t('settings', 'Add user in group')"
  67. label="name" track-by="id" class="multiselect-vue" :limit="2"
  68. :multiple="true" :taggable="settings.isAdmin" :closeOnSelect="false"
  69. @tag="createGroup" @select="addUserGroup" @remove="removeUserGroup">
  70. <span slot="limit" class="multiselect__limit" v-tooltip.auto="formatGroupsTitle(userGroups)">+{{userGroups.length-2}}</span>
  71. <span slot="noResult">{{t('settings', 'No results')}}</span>
  72. </multiselect>
  73. </div>
  74. <div class="subadmins" v-if="subAdminsGroups.length>0 && settings.isAdmin" :class="{'icon-loading-small': loading.subadmins}">
  75. <multiselect :value="userSubAdminsGroups" :options="subAdminsGroups" :disabled="loading.subadmins||loading.all"
  76. :placeholder="t('settings', 'Set user as admin for')"
  77. label="name" track-by="id" class="multiselect-vue" :limit="2"
  78. :multiple="true" :closeOnSelect="false"
  79. @select="addUserSubAdmin" @remove="removeUserSubAdmin">
  80. <span slot="limit" class="multiselect__limit" v-tooltip.auto="formatGroupsTitle(userSubAdminsGroups)">+{{userSubAdminsGroups.length-2}}</span>
  81. <span slot="noResult">{{t('settings', 'No results')}}</span>
  82. </multiselect>
  83. </div>
  84. <div class="quota" :class="{'icon-loading-small': loading.quota}">
  85. <multiselect :value="userQuota" :options="quotaOptions" :disabled="loading.quota||loading.all"
  86. tag-placeholder="create" :placeholder="t('settings', 'Select user quota')"
  87. label="label" track-by="id" class="multiselect-vue"
  88. :allowEmpty="false" :taggable="true"
  89. @tag="validateQuota" @input="setUserQuota">
  90. </multiselect>
  91. <progress class="quota-user-progress" :class="{'warn':usedQuota>80}" :value="usedQuota" max="100"></progress>
  92. </div>
  93. <div class="languages" :class="{'icon-loading-small': loading.languages}"
  94. v-if="showConfig.showLanguages">
  95. <multiselect :value="userLanguage" :options="languages" :disabled="loading.languages||loading.all"
  96. :placeholder="t('settings', 'No language set')"
  97. label="name" track-by="code" class="multiselect-vue"
  98. :allowEmpty="false" group-values="languages" group-label="label"
  99. @input="setUserLanguage">
  100. </multiselect>
  101. </div>
  102. <div class="storageLocation" v-if="showConfig.showStoragePath">{{user.storageLocation}}</div>
  103. <div class="userBackend" v-if="showConfig.showUserBackend">{{user.backend}}</div>
  104. <div class="lastLogin" v-if="showConfig.showLastLogin" v-tooltip.auto="user.lastLogin>0 ? OC.Util.formatDate(user.lastLogin) : ''">
  105. {{user.lastLogin>0 ? OC.Util.relativeModifiedDate(user.lastLogin) : t('settings','Never')}}
  106. </div>
  107. <div class="userActions">
  108. <div class="toggleUserActions" v-if="OC.currentUser !== user.id && user.id !== 'admin' && !loading.all">
  109. <div class="icon-more" v-click-outside="hideMenu" @click="toggleMenu"></div>
  110. <div class="popovermenu" :class="{ 'open': openedMenu }">
  111. <popover-menu :menu="userActions" />
  112. </div>
  113. </div>
  114. </div>
  115. </div>
  116. </template>
  117. <script>
  118. import popoverMenu from '../popoverMenu';
  119. import ClickOutside from 'vue-click-outside';
  120. import Multiselect from 'vue-multiselect';
  121. import Vue from 'vue'
  122. import VTooltip from 'v-tooltip'
  123. Vue.use(VTooltip)
  124. export default {
  125. name: 'userRow',
  126. props: ['user', 'settings', 'groups', 'subAdminsGroups', 'quotaOptions', 'showConfig', 'languages'],
  127. components: {
  128. popoverMenu,
  129. Multiselect
  130. },
  131. directives: {
  132. ClickOutside
  133. },
  134. mounted() {
  135. // required if popup needs to stay opened after menu click
  136. // since we only have disable/delete actions, let's close it directly
  137. // this.popupItem = this.$el;
  138. },
  139. data() {
  140. return {
  141. rand: parseInt(Math.random() * 1000),
  142. openedMenu: false,
  143. loading: {
  144. all: false,
  145. displayName: false,
  146. password: false,
  147. mailAddress: false,
  148. groups: false,
  149. subadmins: false,
  150. quota: false,
  151. delete: false,
  152. disable: false,
  153. languages: false
  154. }
  155. }
  156. },
  157. computed: {
  158. /* USER POPOVERMENU ACTIONS */
  159. userActions() {
  160. return [{
  161. icon: 'icon-delete',
  162. text: t('settings','Delete user'),
  163. action: this.deleteUser
  164. },{
  165. icon: this.user.enabled ? 'icon-close' : 'icon-add',
  166. text: this.user.enabled ? t('settings','Disable user') : t('settings','Enable user'),
  167. action: this.enableDisableUser
  168. }]
  169. },
  170. /* GROUPS MANAGEMENT */
  171. userGroups() {
  172. let userGroups = this.groups.filter(group => this.user.groups.includes(group.id));
  173. return userGroups;
  174. },
  175. userSubAdminsGroups() {
  176. let userSubAdminsGroups = this.subAdminsGroups.filter(group => this.user.subadmin.includes(group.id));
  177. return userSubAdminsGroups;
  178. },
  179. /* QUOTA MANAGEMENT */
  180. usedQuota() {
  181. let quota = this.user.quota.quota;
  182. if (quota > 0) {
  183. quota = Math.min(100, Math.round(this.user.quota.used / quota * 100));
  184. } else {
  185. var usedInGB = this.user.quota.used / (10 * Math.pow(2, 30));
  186. //asymptotic curve approaching 50% at 10GB to visualize used stace with infinite quota
  187. quota = 95 * (1 - (1 / (usedInGB + 1)));
  188. }
  189. return isNaN(quota) ? 0 : quota;
  190. },
  191. // Mapping saved values to objects
  192. userQuota() {
  193. if (this.user.quota.quota >= 0) {
  194. // if value is valid, let's map the quotaOptions or return custom quota
  195. let humanQuota = OC.Util.humanFileSize(this.user.quota.quota);
  196. let userQuota = this.quotaOptions.find(quota => quota.id === humanQuota);
  197. return userQuota ? userQuota : {id:humanQuota, label:humanQuota};
  198. } else if (this.user.quota.quota === 'default') {
  199. // default quota is replaced by the proper value on load
  200. return this.quotaOptions[0];
  201. }
  202. return this.quotaOptions[1]; // unlimited
  203. },
  204. /* PASSWORD POLICY? */
  205. minPasswordLength() {
  206. return this.$store.getters.getPasswordPolicyMinLength;
  207. },
  208. /* LANGUAGE */
  209. userLanguage() {
  210. let availableLanguages = this.languages[0].languages.concat(this.languages[1].languages);
  211. let userLang = availableLanguages.find(lang => lang.code === this.user.language);
  212. if (typeof userLang !== 'object' && this.user.language !== '') {
  213. return {
  214. code: this.user.language,
  215. name: this.user.language
  216. }
  217. } else if(this.user.language === '') {
  218. return false;
  219. }
  220. return userLang;
  221. }
  222. },
  223. methods: {
  224. /* MENU HANDLING */
  225. toggleMenu() {
  226. this.openedMenu = !this.openedMenu;
  227. },
  228. hideMenu() {
  229. this.openedMenu = false;
  230. },
  231. /**
  232. * Generate avatar url
  233. *
  234. * @param {string} user The user name
  235. * @param {int} size Size integer, default 32
  236. * @returns {string}
  237. */
  238. generateAvatar(user, size=32) {
  239. return OC.generateUrl(
  240. '/avatar/{user}/{size}?v={version}',
  241. {
  242. user: user,
  243. size: size,
  244. version: oc_userconfig.avatar.version
  245. }
  246. );
  247. },
  248. /**
  249. * Format array of groups objects to a string for the popup
  250. *
  251. * @param {array} groups The groups
  252. * @returns {string}
  253. */
  254. formatGroupsTitle(groups) {
  255. let names = groups.map(group => group.name);
  256. return names.slice(2,).join(', ');
  257. },
  258. deleteUser() {
  259. this.loading.delete = true;
  260. this.loading.all = true;
  261. let userid = this.user.id;
  262. return this.$store.dispatch('deleteUser', {userid})
  263. .then(() => {
  264. this.loading.delete = false
  265. this.loading.all = false
  266. });
  267. },
  268. enableDisableUser() {
  269. this.loading.delete = true;
  270. this.loading.all = true;
  271. let userid = this.user.id;
  272. let enabled = !this.user.enabled;
  273. return this.$store.dispatch('enableDisableUser', {userid, enabled})
  274. .then(() => {
  275. this.loading.delete = false
  276. this.loading.all = false
  277. });
  278. },
  279. /**
  280. * Set user displayName
  281. *
  282. * @param {string} displayName The display name
  283. * @returns {Promise}
  284. */
  285. updateDisplayName() {
  286. let displayName = this.$refs.displayName.value;
  287. this.loading.displayName = true;
  288. this.$store.dispatch('setUserData', {
  289. userid: this.user.id,
  290. key: 'displayname',
  291. value: displayName
  292. }).then(() => {
  293. this.loading.displayName = false;
  294. this.$refs.displayName.value = displayName;
  295. });
  296. },
  297. /**
  298. * Set user password
  299. *
  300. * @param {string} password The email adress
  301. * @returns {Promise}
  302. */
  303. updatePassword() {
  304. let password = this.$refs.password.value;
  305. this.loading.password = true;
  306. this.$store.dispatch('setUserData', {
  307. userid: this.user.id,
  308. key: 'password',
  309. value: password
  310. }).then(() => {
  311. this.loading.password = false;
  312. this.$refs.password.value = ''; // empty & show placeholder
  313. });
  314. },
  315. /**
  316. * Set user mailAddress
  317. *
  318. * @param {string} mailAddress The email adress
  319. * @returns {Promise}
  320. */
  321. updateEmail() {
  322. let mailAddress = this.$refs.mailAddress.value;
  323. this.loading.mailAddress = true;
  324. this.$store.dispatch('setUserData', {
  325. userid: this.user.id,
  326. key: 'email',
  327. value: mailAddress
  328. }).then(() => {
  329. this.loading.mailAddress = false;
  330. this.$refs.mailAddress.value = mailAddress;
  331. });
  332. },
  333. /**
  334. * Create a new group and add user to it
  335. *
  336. * @param {string} groups Group id
  337. * @returns {Promise}
  338. */
  339. createGroup(gid) {
  340. this.loading = {groups:true, subadmins:true}
  341. this.$store.dispatch('addGroup', gid)
  342. .then(() => {
  343. this.loading = {groups:false, subadmins:false};
  344. let userid = this.user.id;
  345. this.$store.dispatch('addUserGroup', {userid, gid});
  346. })
  347. .catch(() => {
  348. this.loading = {groups:false, subadmins:false};
  349. });
  350. return this.$store.getters.getGroups[this.groups.length];
  351. },
  352. /**
  353. * Add user to group
  354. *
  355. * @param {object} group Group object
  356. * @returns {Promise}
  357. */
  358. addUserGroup(group) {
  359. this.loading.groups = true;
  360. let userid = this.user.id;
  361. let gid = group.id;
  362. return this.$store.dispatch('addUserGroup', {userid, gid})
  363. .then(() => this.loading.groups = false);
  364. },
  365. /**
  366. * Remove user from group
  367. *
  368. * @param {object} group Group object
  369. * @returns {Promise}
  370. */
  371. removeUserGroup(group) {
  372. this.loading.groups = true;
  373. let userid = this.user.id;
  374. let gid = group.id;
  375. return this.$store.dispatch('removeUserGroup', {userid, gid})
  376. .then(() => {
  377. this.loading.groups = false
  378. // remove user from current list if current list is the removed group
  379. if (this.$route.params.selectedGroup === gid) {
  380. this.$store.commit('deleteUser', userid);
  381. }
  382. })
  383. .catch(() => {
  384. this.loading.groups = false
  385. });
  386. },
  387. /**
  388. * Add user to group
  389. *
  390. * @param {object} group Group object
  391. * @returns {Promise}
  392. */
  393. addUserSubAdmin(group) {
  394. this.loading.subadmins = true;
  395. let userid = this.user.id;
  396. let gid = group.id;
  397. return this.$store.dispatch('addUserSubAdmin', {userid, gid})
  398. .then(() => this.loading.subadmins = false);
  399. },
  400. /**
  401. * Remove user from group
  402. *
  403. * @param {object} group Group object
  404. * @returns {Promise}
  405. */
  406. removeUserSubAdmin(group) {
  407. this.loading.subadmins = true;
  408. let userid = this.user.id;
  409. let gid = group.id;
  410. return this.$store.dispatch('removeUserSubAdmin', {userid, gid})
  411. .then(() => this.loading.subadmins = false);
  412. },
  413. /**
  414. * Dispatch quota set request
  415. *
  416. * @param {string|Object} quota Quota in readable format '5 GB' or Object {id: '5 GB', label: '5GB'}
  417. * @returns {string}
  418. */
  419. setUserQuota(quota = 'none') {
  420. this.loading.quota = true;
  421. // ensure we only send the preset id
  422. quota = quota.id ? quota.id : quota;
  423. this.$store.dispatch('setUserData', {
  424. userid: this.user.id,
  425. key: 'quota',
  426. value: quota
  427. }).then(() => this.loading.quota = false);
  428. return quota;
  429. },
  430. /**
  431. * Validate quota string to make sure it's a valid human file size
  432. *
  433. * @param {string} quota Quota in readable format '5 GB'
  434. * @returns {Promise|boolean}
  435. */
  436. validateQuota(quota) {
  437. // only used for new presets sent through @Tag
  438. let validQuota = OC.Util.computerFileSize(quota);
  439. if (validQuota !== null && validQuota >= 0) {
  440. // unify format output
  441. return this.setUserQuota(OC.Util.humanFileSize(OC.Util.computerFileSize(quota)));
  442. }
  443. // if no valid do not change
  444. return false;
  445. },
  446. /**
  447. * Dispatch language set request
  448. *
  449. * @param {Object} lang language object {code:'en', name:'English'}
  450. * @returns {Object}
  451. */
  452. setUserLanguage(lang) {
  453. this.loading.languages = true;
  454. // ensure we only send the preset id
  455. this.$store.dispatch('setUserData', {
  456. userid: this.user.id,
  457. key: 'language',
  458. value: lang.code
  459. }).then(() => this.loading.languages = false);
  460. return lang;
  461. }
  462. }
  463. }
  464. </script>