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.

UserRow.vue 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. <!--
  2. - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
  3. - @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev>
  4. -
  5. - @author John Molakvoæ <skjnldsv@protonmail.com>
  6. - @author Gary Kim <gary@garykim.dev>
  7. -
  8. - @license GNU AGPL version 3 or any later version
  9. -
  10. - This program is free software: you can redistribute it and/or modify
  11. - it under the terms of the GNU Affero General Public License as
  12. - published by the Free Software Foundation, either version 3 of the
  13. - License, or (at your option) any later version.
  14. -
  15. - This program is distributed in the hope that it will be useful,
  16. - but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. - GNU Affero General Public License for more details.
  19. -
  20. - You should have received a copy of the GNU Affero General Public License
  21. - along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. -
  23. -->
  24. <template>
  25. <!-- Obfuscated user: Logged in user does not have permissions to see all of the data -->
  26. <div v-if="Object.keys(user).length ===1" :data-id="user.id" class="row">
  27. <div :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}"
  28. class="avatar">
  29. <img v-if="!loading.delete && !loading.disable && !loading.wipe"
  30. :src="generateAvatar(user.id, 32)"
  31. :srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
  32. alt=""
  33. height="32"
  34. width="32">
  35. </div>
  36. <div class="name">
  37. {{ user.id }}
  38. </div>
  39. <div class="obfuscated">
  40. {{ t('settings','You do not have permissions to see the details of this user') }}
  41. </div>
  42. </div>
  43. <!-- User full data -->
  44. <UserRowSimple
  45. v-else-if="!editing"
  46. :editing.sync="editing"
  47. :feedback-message="feedbackMessage"
  48. :groups="groups"
  49. :languages="languages"
  50. :loading="loading"
  51. :opened-menu="openedMenu"
  52. :settings="settings"
  53. :show-config="showConfig"
  54. :sub-admins-groups="subAdminsGroups"
  55. :user-actions="userActions"
  56. :user="user"
  57. :class="{'row--menu-opened': openedMenu}"
  58. @hideMenu="hideMenu"
  59. @toggleMenu="toggleMenu" />
  60. <div v-else
  61. :class="{
  62. 'disabled': loading.delete || loading.disable,
  63. 'row--menu-opened': openedMenu
  64. }"
  65. :data-id="user.id"
  66. class="row row--editable">
  67. <div :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}"
  68. class="avatar">
  69. <img v-if="!loading.delete && !loading.disable && !loading.wipe"
  70. :src="generateAvatar(user.id, 32)"
  71. :srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
  72. alt=""
  73. height="32"
  74. width="32">
  75. </div>
  76. <!-- dirty hack to ellipsis on two lines -->
  77. <div v-if="user.backendCapabilities.setDisplayName" class="displayName">
  78. <form
  79. :class="{'icon-loading-small': loading.displayName}"
  80. class="displayName"
  81. @submit.prevent="updateDisplayName">
  82. <input
  83. :id="'displayName'+user.id+rand"
  84. ref="displayName"
  85. :disabled="loading.displayName||loading.all"
  86. :value="user.displayname"
  87. autocapitalize="off"
  88. autocomplete="off"
  89. autocorrect="off"
  90. spellcheck="false"
  91. type="text">
  92. <input
  93. class="icon-confirm"
  94. type="submit"
  95. value="">
  96. </form>
  97. </div>
  98. <div v-else class="name">
  99. {{ user.id }}
  100. <div class="displayName subtitle">
  101. <div v-tooltip="user.displayname.length > 20 ? user.displayname : ''" class="cellText">
  102. {{ user.displayname }}
  103. </div>
  104. </div>
  105. </div>
  106. <form v-if="settings.canChangePassword && user.backendCapabilities.setPassword"
  107. :class="{'icon-loading-small': loading.password}"
  108. class="password"
  109. @submit.prevent="updatePassword">
  110. <input :id="'password'+user.id+rand"
  111. ref="password"
  112. :disabled="loading.password || loading.all"
  113. :minlength="minPasswordLength"
  114. :placeholder="t('settings', 'Add new password')"
  115. autocapitalize="off"
  116. autocomplete="new-password"
  117. autocorrect="off"
  118. required
  119. spellcheck="false"
  120. type="password"
  121. value="">
  122. <input class="icon-confirm" type="submit" value="">
  123. </form>
  124. <div v-else />
  125. <form :class="{'icon-loading-small': loading.mailAddress}"
  126. class="mailAddress"
  127. @submit.prevent="updateEmail">
  128. <input :id="'mailAddress'+user.id+rand"
  129. ref="mailAddress"
  130. :disabled="loading.mailAddress||loading.all"
  131. :placeholder="t('settings', 'Add new email address')"
  132. :value="user.email"
  133. autocapitalize="off"
  134. autocomplete="new-password"
  135. autocorrect="off"
  136. spellcheck="false"
  137. type="email">
  138. <input class="icon-confirm" type="submit" value="">
  139. </form>
  140. <div :class="{'icon-loading-small': loading.groups}" class="groups">
  141. <Multiselect :close-on-select="false"
  142. :disabled="loading.groups||loading.all"
  143. :limit="2"
  144. :multiple="true"
  145. :options="availableGroups"
  146. :placeholder="t('settings', 'Add user in group')"
  147. :tag-width="60"
  148. :taggable="settings.isAdmin"
  149. :value="userGroups"
  150. class="multiselect-vue"
  151. label="name"
  152. tag-placeholder="create"
  153. track-by="id"
  154. @remove="removeUserGroup"
  155. @select="addUserGroup"
  156. @tag="createGroup">
  157. <span slot="noResult">{{ t('settings', 'No results') }}</span>
  158. </Multiselect>
  159. </div>
  160. <div v-if="subAdminsGroups.length>0 && settings.isAdmin"
  161. :class="{'icon-loading-small': loading.subadmins}"
  162. class="subadmins">
  163. <Multiselect :close-on-select="false"
  164. :disabled="loading.subadmins||loading.all"
  165. :limit="2"
  166. :multiple="true"
  167. :options="subAdminsGroups"
  168. :placeholder="t('settings', 'Set user as admin for')"
  169. :tag-width="60"
  170. :value="userSubAdminsGroups"
  171. class="multiselect-vue"
  172. label="name"
  173. track-by="id"
  174. @remove="removeUserSubAdmin"
  175. @select="addUserSubAdmin">
  176. <span slot="noResult">{{ t('settings', 'No results') }}</span>
  177. </Multiselect>
  178. </div>
  179. <div v-tooltip.auto="usedSpace"
  180. :class="{'icon-loading-small': loading.quota}"
  181. class="quota">
  182. <Multiselect :allow-empty="false"
  183. :disabled="loading.quota||loading.all"
  184. :options="quotaOptions"
  185. :placeholder="t('settings', 'Select user quota')"
  186. :taggable="true"
  187. :value="userQuota"
  188. class="multiselect-vue"
  189. label="label"
  190. tag-placeholder="create"
  191. track-by="id"
  192. @input="setUserQuota"
  193. @tag="validateQuota" />
  194. </div>
  195. <div v-if="showConfig.showLanguages"
  196. :class="{'icon-loading-small': loading.languages}"
  197. class="languages">
  198. <Multiselect :allow-empty="false"
  199. :disabled="loading.languages||loading.all"
  200. :options="languages"
  201. :placeholder="t('settings', 'No language set')"
  202. :value="userLanguage"
  203. class="multiselect-vue"
  204. group-label="label"
  205. group-values="languages"
  206. label="name"
  207. track-by="code"
  208. @input="setUserLanguage" />
  209. </div>
  210. <!-- don't show this on edit mode -->
  211. <div v-if="showConfig.showStoragePath || showConfig.showUserBackend"
  212. class="storageLocation" />
  213. <div v-if="showConfig.showLastLogin" />
  214. <div class="userActions">
  215. <div v-if="!loading.all"
  216. class="toggleUserActions">
  217. <Actions>
  218. <ActionButton icon="icon-checkmark"
  219. @click="editing = false">
  220. {{ t('settings', 'Done') }}
  221. </ActionButton>
  222. </Actions>
  223. <div v-click-outside="hideMenu" class="userPopoverMenuWrapper">
  224. <div class="icon-more"
  225. @click="toggleMenu" />
  226. <div :class="{ 'open': openedMenu }" class="popovermenu">
  227. <PopoverMenu :menu="userActions" />
  228. </div>
  229. </div>
  230. </div>
  231. <div :style="{opacity: feedbackMessage !== '' ? 1 : 0}"
  232. class="feedback">
  233. <div class="icon-checkmark" />
  234. {{ feedbackMessage }}
  235. </div>
  236. </div>
  237. </div>
  238. </template>
  239. <script>
  240. import ClickOutside from 'vue-click-outside'
  241. import Vue from 'vue'
  242. import VTooltip from 'v-tooltip'
  243. import {
  244. PopoverMenu,
  245. Multiselect,
  246. Actions,
  247. ActionButton,
  248. } from '@nextcloud/vue'
  249. import UserRowSimple from './UserRowSimple'
  250. import UserRowMixin from '../../mixins/UserRowMixin'
  251. Vue.use(VTooltip)
  252. export default {
  253. name: 'UserRow',
  254. components: {
  255. UserRowSimple,
  256. PopoverMenu,
  257. Actions,
  258. ActionButton,
  259. Multiselect,
  260. },
  261. directives: {
  262. ClickOutside,
  263. },
  264. mixins: [UserRowMixin],
  265. props: {
  266. user: {
  267. type: Object,
  268. required: true,
  269. },
  270. settings: {
  271. type: Object,
  272. default: () => ({}),
  273. },
  274. groups: {
  275. type: Array,
  276. default: () => [],
  277. },
  278. subAdminsGroups: {
  279. type: Array,
  280. default: () => [],
  281. },
  282. quotaOptions: {
  283. type: Array,
  284. default: () => [],
  285. },
  286. showConfig: {
  287. type: Object,
  288. default: () => ({}),
  289. },
  290. languages: {
  291. type: Array,
  292. required: true,
  293. },
  294. externalActions: {
  295. type: Array,
  296. default: () => [],
  297. },
  298. },
  299. data() {
  300. return {
  301. rand: parseInt(Math.random() * 1000),
  302. openedMenu: false,
  303. feedbackMessage: '',
  304. editing: false,
  305. loading: {
  306. all: false,
  307. displayName: false,
  308. password: false,
  309. mailAddress: false,
  310. groups: false,
  311. subadmins: false,
  312. quota: false,
  313. delete: false,
  314. disable: false,
  315. languages: false,
  316. wipe: false,
  317. },
  318. }
  319. },
  320. computed: {
  321. /* USER POPOVERMENU ACTIONS */
  322. userActions() {
  323. const actions = [
  324. {
  325. icon: 'icon-delete',
  326. text: t('settings', 'Delete user'),
  327. action: this.deleteUser,
  328. },
  329. {
  330. icon: 'icon-delete',
  331. text: t('settings', 'Wipe all devices'),
  332. action: this.wipeUserDevices,
  333. },
  334. {
  335. icon: this.user.enabled ? 'icon-close' : 'icon-add',
  336. text: this.user.enabled ? t('settings', 'Disable user') : t('settings', 'Enable user'),
  337. action: this.enableDisableUser,
  338. },
  339. ]
  340. if (this.user.email !== null && this.user.email !== '') {
  341. actions.push({
  342. icon: 'icon-mail',
  343. text: t('settings', 'Resend welcome email'),
  344. action: this.sendWelcomeMail,
  345. })
  346. }
  347. return actions.concat(this.externalActions)
  348. },
  349. },
  350. methods: {
  351. /* MENU HANDLING */
  352. toggleMenu() {
  353. this.openedMenu = !this.openedMenu
  354. },
  355. hideMenu() {
  356. this.openedMenu = false
  357. },
  358. wipeUserDevices() {
  359. const userid = this.user.id
  360. OC.dialogs.confirmDestructive(
  361. t('settings', 'In case of lost device or exiting the organization, this can remotely wipe the Nextcloud data from all devices associated with {userid}. Only works if the devices are connected to the internet.', { userid: userid }),
  362. t('settings', 'Remote wipe of devices'),
  363. {
  364. type: OC.dialogs.YES_NO_BUTTONS,
  365. confirm: t('settings', 'Wipe {userid}\'s devices', { userid: userid }),
  366. confirmClasses: 'error',
  367. cancel: t('settings', 'Cancel'),
  368. },
  369. (result) => {
  370. if (result) {
  371. this.loading.wipe = true
  372. this.loading.all = true
  373. this.$store.dispatch('wipeUserDevices', userid)
  374. .then(() => {
  375. this.loading.wipe = false
  376. this.loading.all = false
  377. })
  378. }
  379. },
  380. true
  381. )
  382. },
  383. deleteUser() {
  384. const userid = this.user.id
  385. OC.dialogs.confirmDestructive(
  386. t('settings', 'Fully delete {userid}\'s account including all their personal files, app data, etc.', { userid: userid }),
  387. t('settings', 'Account deletion'),
  388. {
  389. type: OC.dialogs.YES_NO_BUTTONS,
  390. confirm: t('settings', 'Delete {userid}\'s account', { userid: userid }),
  391. confirmClasses: 'error',
  392. cancel: t('settings', 'Cancel'),
  393. },
  394. (result) => {
  395. if (result) {
  396. this.loading.delete = true
  397. this.loading.all = true
  398. return this.$store.dispatch('deleteUser', userid)
  399. .then(() => {
  400. this.loading.delete = false
  401. this.loading.all = false
  402. })
  403. }
  404. },
  405. true
  406. )
  407. },
  408. enableDisableUser() {
  409. this.loading.delete = true
  410. this.loading.all = true
  411. const userid = this.user.id
  412. const enabled = !this.user.enabled
  413. return this.$store.dispatch('enableDisableUser', {
  414. userid,
  415. enabled,
  416. })
  417. .then(() => {
  418. this.loading.delete = false
  419. this.loading.all = false
  420. })
  421. },
  422. /**
  423. * Set user displayName
  424. *
  425. * @param {string} displayName The display name
  426. */
  427. updateDisplayName() {
  428. const displayName = this.$refs.displayName.value
  429. this.loading.displayName = true
  430. this.$store.dispatch('setUserData', {
  431. userid: this.user.id,
  432. key: 'displayname',
  433. value: displayName,
  434. }).then(() => {
  435. this.loading.displayName = false
  436. this.$refs.displayName.value = displayName
  437. })
  438. },
  439. /**
  440. * Set user password
  441. *
  442. * @param {string} password The email adress
  443. */
  444. updatePassword() {
  445. const password = this.$refs.password.value
  446. this.loading.password = true
  447. this.$store.dispatch('setUserData', {
  448. userid: this.user.id,
  449. key: 'password',
  450. value: password,
  451. }).then(() => {
  452. this.loading.password = false
  453. this.$refs.password.value = '' // empty & show placeholder
  454. })
  455. },
  456. /**
  457. * Set user mailAddress
  458. *
  459. * @param {string} mailAddress The email adress
  460. */
  461. updateEmail() {
  462. const mailAddress = this.$refs.mailAddress.value
  463. this.loading.mailAddress = true
  464. this.$store.dispatch('setUserData', {
  465. userid: this.user.id,
  466. key: 'email',
  467. value: mailAddress,
  468. }).then(() => {
  469. this.loading.mailAddress = false
  470. this.$refs.mailAddress.value = mailAddress
  471. })
  472. },
  473. /**
  474. * Create a new group and add user to it
  475. *
  476. * @param {string} gid Group id
  477. */
  478. async createGroup(gid) {
  479. this.loading = { groups: true, subadmins: true }
  480. try {
  481. await this.$store.dispatch('addGroup', gid)
  482. const userid = this.user.id
  483. await this.$store.dispatch('addUserGroup', { userid, gid })
  484. } catch (error) {
  485. console.error(error)
  486. } finally {
  487. this.loading = { groups: false, subadmins: false }
  488. }
  489. return this.$store.getters.getGroups[this.groups.length]
  490. },
  491. /**
  492. * Add user to group
  493. *
  494. * @param {object} group Group object
  495. */
  496. async addUserGroup(group) {
  497. if (group.canAdd === false) {
  498. return false
  499. }
  500. this.loading.groups = true
  501. const userid = this.user.id
  502. const gid = group.id
  503. try {
  504. await this.$store.dispatch('addUserGroup', { userid, gid })
  505. } catch (error) {
  506. console.error(error)
  507. } finally {
  508. this.loading.groups = false
  509. }
  510. },
  511. /**
  512. * Remove user from group
  513. *
  514. * @param {object} group Group object
  515. */
  516. async removeUserGroup(group) {
  517. if (group.canRemove === false) {
  518. return false
  519. }
  520. this.loading.groups = true
  521. const userid = this.user.id
  522. const gid = group.id
  523. try {
  524. await this.$store.dispatch('removeUserGroup', {
  525. userid,
  526. gid,
  527. })
  528. this.loading.groups = false
  529. // remove user from current list if current list is the removed group
  530. if (this.$route.params.selectedGroup === gid) {
  531. this.$store.commit('deleteUser', userid)
  532. }
  533. } catch {
  534. this.loading.groups = false
  535. }
  536. },
  537. /**
  538. * Add user to group
  539. *
  540. * @param {object} group Group object
  541. */
  542. async addUserSubAdmin(group) {
  543. this.loading.subadmins = true
  544. const userid = this.user.id
  545. const gid = group.id
  546. try {
  547. await this.$store.dispatch('addUserSubAdmin', {
  548. userid,
  549. gid,
  550. })
  551. this.loading.subadmins = false
  552. } catch (error) {
  553. console.error(error)
  554. }
  555. },
  556. /**
  557. * Remove user from group
  558. *
  559. * @param {object} group Group object
  560. */
  561. async removeUserSubAdmin(group) {
  562. this.loading.subadmins = true
  563. const userid = this.user.id
  564. const gid = group.id
  565. try {
  566. await this.$store.dispatch('removeUserSubAdmin', {
  567. userid,
  568. gid,
  569. })
  570. } catch (error) {
  571. console.error(error)
  572. } finally {
  573. this.loading.subadmins = false
  574. }
  575. },
  576. /**
  577. * Dispatch quota set request
  578. *
  579. * @param {string|Object} quota Quota in readable format '5 GB' or Object {id: '5 GB', label: '5GB'}
  580. * @returns {string}
  581. */
  582. async setUserQuota(quota = 'none') {
  583. this.loading.quota = true
  584. // ensure we only send the preset id
  585. quota = quota.id ? quota.id : quota
  586. try {
  587. await this.$store.dispatch('setUserData', {
  588. userid: this.user.id,
  589. key: 'quota',
  590. value: quota,
  591. })
  592. } catch (error) {
  593. console.error(error)
  594. } finally {
  595. this.loading.quota = false
  596. }
  597. return quota
  598. },
  599. /**
  600. * Validate quota string to make sure it's a valid human file size
  601. *
  602. * @param {string} quota Quota in readable format '5 GB'
  603. * @returns {Promise|boolean}
  604. */
  605. validateQuota(quota) {
  606. // only used for new presets sent through @Tag
  607. const validQuota = OC.Util.computerFileSize(quota)
  608. if (validQuota !== null && validQuota >= 0) {
  609. // unify format output
  610. return this.setUserQuota(OC.Util.humanFileSize(OC.Util.computerFileSize(quota)))
  611. }
  612. // if no valid do not change
  613. return false
  614. },
  615. /**
  616. * Dispatch language set request
  617. *
  618. * @param {Object} lang language object {code:'en', name:'English'}
  619. * @returns {Object}
  620. */
  621. async setUserLanguage(lang) {
  622. this.loading.languages = true
  623. // ensure we only send the preset id
  624. try {
  625. await this.$store.dispatch('setUserData', {
  626. userid: this.user.id,
  627. key: 'language',
  628. value: lang.code,
  629. })
  630. } catch (error) {
  631. console.error(error)
  632. } finally {
  633. this.loading.languages = false
  634. }
  635. return lang
  636. },
  637. /**
  638. * Dispatch new welcome mail request
  639. */
  640. sendWelcomeMail() {
  641. this.loading.all = true
  642. this.$store.dispatch('sendWelcomeMail', this.user.id)
  643. .then(success => {
  644. if (success) {
  645. // Show feedback to indicate the success
  646. this.feedbackMessage = t('setting', 'Welcome mail sent!')
  647. setTimeout(() => {
  648. this.feedbackMessage = ''
  649. }, 2000)
  650. }
  651. this.loading.all = false
  652. })
  653. },
  654. },
  655. }
  656. </script>
  657. <style scoped lang="scss">
  658. // Force menu to be above other rows
  659. .row--menu-opened {
  660. z-index: 1 !important;
  661. }
  662. .row::v-deep .multiselect__single {
  663. z-index: auto !important;
  664. }
  665. </style>