UserEditOverlay.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829
  1. <template lang="pug">
  2. q-layout(view='hHh lpR fFf', container)
  3. q-header.card-header.q-px-md.q-py-sm
  4. q-icon(name='img:/_assets/icons/fluent-account.svg', left, size='md')
  5. div
  6. span {{t(`admin.users.edit`)}}
  7. .text-caption {{state.user.name}}
  8. q-space
  9. q-btn-group(push)
  10. q-btn(
  11. push
  12. color='grey-6'
  13. text-color='white'
  14. :aria-label='t(`common.actions.refresh`)'
  15. icon='las la-redo-alt'
  16. @click='fetchUser'
  17. :loading='state.loading > 0'
  18. )
  19. q-tooltip(anchor='center left', self='center right') {{t(`common.actions.refresh`)}}
  20. q-btn(
  21. push
  22. color='white'
  23. text-color='grey-7'
  24. :label='t(`common.actions.close`)'
  25. :aria-label='t(`common.actions.close`)'
  26. icon='las la-times'
  27. @click='close'
  28. )
  29. q-btn(
  30. push
  31. color='positive'
  32. text-color='white'
  33. :label='t(`common.actions.save`)'
  34. :aria-label='t(`common.actions.save`)'
  35. icon='las la-check'
  36. @click='save()'
  37. :disabled='state.loading > 0'
  38. )
  39. q-drawer.bg-dark-6(:model-value='true', :width='250', dark)
  40. q-list(padding, v-if='state.loading < 1')
  41. q-item(
  42. v-for='sc of sections'
  43. :key='`section-` + sc.key'
  44. clickable
  45. :to='{ params: { section: sc.key } }'
  46. active-class='bg-primary text-white'
  47. :disabled='sc.disabled'
  48. )
  49. q-item-section(side)
  50. q-icon(:name='sc.icon', color='white')
  51. q-item-section {{sc.text}}
  52. q-page-container
  53. q-page(v-if='state.loading > 0')
  54. .flex.q-pa-lg.items-center
  55. q-spinner-tail(color='primary', size='32px', :thickness='2')
  56. .text-caption.text-primary.q-pl-md: strong {{t('admin.users.loading')}}
  57. q-page(v-else-if='route.params.section === `overview`')
  58. .q-pa-md
  59. .row.q-col-gutter-md
  60. .col-12.col-lg-8
  61. q-card.shadow-1.q-pb-sm
  62. q-card-section
  63. .text-subtitle1 {{t('admin.users.profile')}}
  64. q-item
  65. blueprint-icon(icon='contact')
  66. q-item-section
  67. q-item-label {{t(`admin.users.name`)}}
  68. q-item-label(caption) {{t(`admin.users.nameHint`)}}
  69. q-item-section
  70. q-input(
  71. outlined
  72. v-model='state.user.name'
  73. dense
  74. :rules=`[
  75. val => invalidCharsRegex.test(val) || t('admin.users.nameInvalidChars')
  76. ]`
  77. hide-bottom-space
  78. :aria-label='t(`admin.users.name`)'
  79. )
  80. q-separator.q-my-sm(inset)
  81. q-item
  82. blueprint-icon(icon='envelope')
  83. q-item-section
  84. q-item-label {{t(`admin.users.email`)}}
  85. q-item-label(caption) {{t(`admin.users.emailHint`)}}
  86. q-item-section
  87. q-input(
  88. outlined
  89. v-model='state.user.email'
  90. dense
  91. :aria-label='t(`admin.users.email`)'
  92. )
  93. template(v-if='state.user.meta')
  94. q-separator.q-my-sm(inset)
  95. q-item
  96. blueprint-icon(icon='address')
  97. q-item-section
  98. q-item-label {{t(`admin.users.location`)}}
  99. q-item-label(caption) {{t(`admin.users.locationHint`)}}
  100. q-item-section
  101. q-input(
  102. outlined
  103. v-model='state.user.meta.location'
  104. dense
  105. :aria-label='t(`admin.users.location`)'
  106. )
  107. q-separator.q-my-sm(inset)
  108. q-item
  109. blueprint-icon(icon='new-job')
  110. q-item-section
  111. q-item-label {{t(`admin.users.jobTitle`)}}
  112. q-item-label(caption) {{t(`admin.users.jobTitleHint`)}}
  113. q-item-section
  114. q-input(
  115. outlined
  116. v-model='state.user.meta.jobTitle'
  117. dense
  118. :aria-label='t(`admin.users.jobTitle`)'
  119. )
  120. q-card.shadow-1.q-pb-sm.q-mt-md(v-if='state.user.meta')
  121. q-card-section
  122. .text-subtitle1 {{t('admin.users.preferences')}}
  123. q-item
  124. blueprint-icon(icon='timezone')
  125. q-item-section
  126. q-item-label {{t(`admin.users.timezone`)}}
  127. q-item-label(caption) {{t(`admin.users.timezoneHint`)}}
  128. q-item-section
  129. q-select(
  130. outlined
  131. v-model='state.user.prefs.timezone'
  132. :options='dataStore.timezones'
  133. option-value='value'
  134. option-label='text'
  135. emit-value
  136. map-options
  137. dense
  138. options-dense
  139. :aria-label='t(`admin.users.timezone`)'
  140. )
  141. q-separator.q-my-sm(inset)
  142. q-item
  143. blueprint-icon(icon='calendar')
  144. q-item-section
  145. q-item-label {{t(`admin.users.dateFormat`)}}
  146. q-item-label(caption) {{t(`admin.users.dateFormatHint`)}}
  147. q-item-section
  148. q-select(
  149. outlined
  150. v-model='state.user.prefs.dateFormat'
  151. emit-value
  152. map-options
  153. dense
  154. :aria-label='t(`admin.users.dateFormat`)'
  155. :options=`[
  156. { label: t('profile.localeDefault'), value: '' },
  157. { label: 'DD/MM/YYYY', value: 'DD/MM/YYYY' },
  158. { label: 'DD.MM.YYYY', value: 'DD.MM.YYYY' },
  159. { label: 'MM/DD/YYYY', value: 'MM/DD/YYYY' },
  160. { label: 'YYYY-MM-DD', value: 'YYYY-MM-DD' },
  161. { label: 'YYYY/MM/DD', value: 'YYYY/MM/DD' }
  162. ]`
  163. )
  164. q-separator.q-my-sm(inset)
  165. q-item
  166. blueprint-icon(icon='clock')
  167. q-item-section
  168. q-item-label {{t(`admin.users.timeFormat`)}}
  169. q-item-label(caption) {{t(`admin.users.timeFormatHint`)}}
  170. q-item-section.col-auto
  171. q-btn-toggle(
  172. v-model='state.user.prefs.timeFormat'
  173. push
  174. glossy
  175. no-caps
  176. toggle-color='primary'
  177. :options=`[
  178. { label: t('profile.timeFormat12h'), value: '12h' },
  179. { label: t('profile.timeFormat24h'), value: '24h' }
  180. ]`
  181. )
  182. q-separator.q-my-sm(inset)
  183. q-item(tag='label', v-ripple)
  184. blueprint-icon(icon='light-on')
  185. q-item-section
  186. q-item-label {{t(`admin.users.darkMode`)}}
  187. q-item-label(caption) {{t(`admin.users.darkModeHint`)}}
  188. q-item-section(avatar)
  189. q-toggle(
  190. v-model='state.user.prefs.darkMode'
  191. color='primary'
  192. checked-icon='las la-check'
  193. unchecked-icon='las la-times'
  194. :aria-label='t(`admin.users.darkMode`)'
  195. )
  196. .col-12.col-lg-4
  197. q-card.shadow-1.q-pb-sm
  198. q-card-section
  199. .text-subtitle1 {{t('admin.users.info')}}
  200. q-item
  201. blueprint-icon(icon='person', :hue-rotate='-45')
  202. q-item-section
  203. q-item-label {{t(`common.field.id`)}}
  204. q-item-label: strong {{state.user.id}}
  205. q-separator.q-my-sm(inset)
  206. q-item
  207. blueprint-icon(icon='calendar-plus', :hue-rotate='-45')
  208. q-item-section
  209. q-item-label {{t(`common.field.createdOn`)}}
  210. q-item-label: strong {{humanizeDate(state.user.createdAt)}}
  211. q-separator.q-my-sm(inset)
  212. q-item
  213. blueprint-icon(icon='summertime', :hue-rotate='-45')
  214. q-item-section
  215. q-item-label {{t(`common.field.lastUpdated`)}}
  216. q-item-label: strong {{humanizeDate(state.user.updatedAt)}}
  217. q-separator.q-my-sm(inset)
  218. q-item
  219. blueprint-icon(icon='enter', :hue-rotate='-45')
  220. q-item-section
  221. q-item-label {{t(`admin.users.lastLoginAt`)}}
  222. q-item-label: strong {{humanizeDate(state.user.lastLoginAt)}}
  223. q-card.shadow-1.q-pb-sm.q-mt-md(v-if='state.user.meta')
  224. q-card-section
  225. .text-subtitle1 {{t('admin.users.notes')}}
  226. q-input.q-mt-sm(
  227. outlined
  228. v-model='state.user.meta.notes'
  229. type='textarea'
  230. :aria-label='t(`admin.users.notes`)'
  231. input-style='min-height: 243px'
  232. :hint='t(`admin.users.noteHint`)'
  233. )
  234. q-page(v-else-if='route.params.section === `activity`')
  235. span ---
  236. q-page(v-else-if='route.params.section === `auth`')
  237. .q-pa-md
  238. .row.q-col-gutter-md
  239. .col-12.col-lg-7
  240. q-card.shadow-1.q-pb-sm
  241. q-card-section
  242. .text-subtitle1 {{t('admin.users.passAuth')}}
  243. q-item
  244. blueprint-icon(icon='password', :hue-rotate='45')
  245. q-item-section
  246. q-item-label {{t(`admin.users.changePassword`)}}
  247. q-item-label(caption) {{t(`admin.users.changePasswordHint`)}}
  248. q-item-label(caption): strong(:class='localAuth.password ? `text-positive` : `text-negative`') {{localAuth.password ? t(`admin.users.pwdSet`) : t(`admin.users.pwdNotSet`)}}
  249. q-item-section(side)
  250. q-btn.acrylic-btn(
  251. flat
  252. icon='las la-arrow-circle-right'
  253. color='primary'
  254. @click='changePassword'
  255. :label='t(`common.actions.proceed`)'
  256. )
  257. q-separator.q-my-sm(inset)
  258. q-item(tag='label', v-ripple)
  259. blueprint-icon(icon='password-reset')
  260. q-item-section
  261. q-item-label {{t(`admin.users.mustChangePwd`)}}
  262. q-item-label(caption) {{t(`admin.users.mustChangePwdHint`)}}
  263. q-item-section(avatar)
  264. q-toggle(
  265. v-model='localAuth.mustChangePwd'
  266. color='primary'
  267. checked-icon='las la-check'
  268. unchecked-icon='las la-times'
  269. :aria-label='t(`admin.users.mustChangePwd`)'
  270. )
  271. q-separator.q-my-sm(inset)
  272. q-item(tag='label', v-ripple)
  273. blueprint-icon(icon='key')
  274. q-item-section
  275. q-item-label {{t(`admin.users.pwdAuthRestrict`)}}
  276. q-item-label(caption) {{t(`admin.users.pwdAuthRestrictHint`)}}
  277. q-item-section(avatar)
  278. q-toggle(
  279. v-model='localAuth.restrictLogin'
  280. color='primary'
  281. checked-icon='las la-check'
  282. unchecked-icon='las la-times'
  283. :aria-label='t(`admin.users.pwdAuthRestrict`)'
  284. )
  285. q-card.shadow-1.q-pb-sm.q-mt-md
  286. q-card-section
  287. .text-subtitle1 {{t('admin.users.tfa')}}
  288. q-item(tag='label', v-ripple)
  289. blueprint-icon(icon='key')
  290. q-item-section
  291. q-item-label {{t(`admin.users.tfaRequired`)}}
  292. q-item-label(caption) {{t(`admin.users.tfaRequiredHint`)}}
  293. q-item-section(avatar)
  294. q-toggle(
  295. v-model='localAuth.tfaRequired'
  296. color='primary'
  297. checked-icon='las la-check'
  298. unchecked-icon='las la-times'
  299. :aria-label='t(`admin.users.tfaRequired`)'
  300. )
  301. q-separator.q-my-sm(inset)
  302. q-item
  303. blueprint-icon(icon='password', :hue-rotate='45')
  304. q-item-section
  305. q-item-label {{t(`admin.users.tfaInvalidate`)}}
  306. q-item-label(caption) {{t(`admin.users.tfaInvalidateHint`)}}
  307. q-item-label(caption): strong(:class='localAuth.tfaSecret ? `text-positive` : `text-negative`') {{localAuth.tfaSecret ? t(`admin.users.tfaSet`) : t(`admin.users.tfaNotSet`)}}
  308. q-item-section(side)
  309. q-btn.acrylic-btn(
  310. flat
  311. icon='las la-arrow-circle-right'
  312. color='primary'
  313. @click='invalidateTFA'
  314. :label='t(`common.actions.proceed`)'
  315. )
  316. .col-12.col-lg-5
  317. q-card.shadow-1.q-pb-sm
  318. q-card-section
  319. .text-subtitle1 {{t('admin.users.linkedProviders')}}
  320. q-banner.q-mt-md(
  321. v-if='linkedAuthProviders.length < 1'
  322. rounded
  323. :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-2 text-grey-7`'
  324. ) {{t('admin.users.noLinkedProviders')}}
  325. template(
  326. v-for='(prv, idx) in linkedAuthProviders'
  327. :key='prv._id'
  328. )
  329. q-separator.q-my-sm(inset, v-if='idx > 0')
  330. q-item
  331. blueprint-icon(icon='google', :hue-rotate='-45')
  332. q-item-section
  333. q-item-label {{prv._moduleName}}
  334. q-item-label(caption) {{prv.key}}
  335. q-page(v-else-if='route.params.section === `groups`')
  336. .q-pa-md
  337. .row.q-col-gutter-md
  338. .col-12.col-lg-8
  339. q-card.shadow-1.q-pb-sm
  340. q-card-section
  341. .text-subtitle1 {{t('admin.users.groups')}}
  342. template(
  343. v-for='(grp, idx) of state.user.groups'
  344. :key='grp.id'
  345. )
  346. q-separator.q-my-sm(inset, v-if='idx > 0')
  347. q-item
  348. blueprint-icon(icon='team', :hue-rotate='-45')
  349. q-item-section
  350. q-item-label {{grp.name}}
  351. q-item-section(side)
  352. q-btn.acrylic-btn(
  353. flat
  354. icon='las la-times'
  355. color='accent'
  356. @click='unassignGroup(grp.id)'
  357. :aria-label='t(`admin.users.unassignGroup`)'
  358. )
  359. q-tooltip(anchor='center left' self='center right') {{t('admin.users.unassignGroup')}}
  360. q-card.shadow-1.q-py-sm.q-mt-md
  361. q-item
  362. blueprint-icon(icon='join')
  363. q-item-section
  364. q-select(
  365. outlined
  366. :options='state.groups'
  367. v-model='state.groupToAdd'
  368. map-options
  369. emit-value
  370. option-value='id'
  371. option-label='name'
  372. options-dense
  373. dense
  374. hide-bottom-space
  375. :label='t(`admin.users.groups`)'
  376. :aria-label='t(`admin.users.groups`)'
  377. :loading='state.loading > 0'
  378. )
  379. q-item-section(side)
  380. q-btn(
  381. unelevated
  382. icon='las la-plus'
  383. :label='t(`admin.users.assignGroup`)'
  384. color='primary'
  385. @click='assignGroup'
  386. )
  387. q-page(v-else-if='route.params.section === `metadata`')
  388. .q-pa-md
  389. .row.q-col-gutter-md
  390. .col-12.col-lg-8
  391. q-card.shadow-1.q-pb-sm
  392. q-card-section.flex.items-center
  393. .text-subtitle1 {{t('admin.users.metadata')}}
  394. q-space
  395. q-badge(
  396. v-if='state.metadataInvalidJSON'
  397. color='negative'
  398. )
  399. q-icon.q-mr-xs(name='las la-exclamation-triangle', size='20px')
  400. span {{t('admin.users.invalidJSON')}}
  401. q-badge.q-py-xs(
  402. v-else
  403. label='JSON'
  404. color='positive'
  405. )
  406. q-item
  407. q-item-section
  408. q-no-ssr(:placeholder='t(`common.loading`)')
  409. util-code-editor.admin-theme-cm(
  410. v-model='metadata'
  411. language='json'
  412. :min-height='500'
  413. )
  414. q-page(v-else-if='route.params.section === `operations`')
  415. .q-pa-md
  416. .row.q-col-gutter-md
  417. .col-12.col-lg-8
  418. q-card.shadow-1.q-pb-sm
  419. q-card-section
  420. .text-subtitle1 {{t('admin.users.operations')}}
  421. q-item
  422. blueprint-icon(icon='email-open', :hue-rotate='45')
  423. q-item-section
  424. q-item-label {{t(`admin.users.sendWelcomeEmail`)}}
  425. q-item-label(caption) {{t(`admin.users.sendWelcomeEmailAltHint`)}}
  426. q-item-section(side)
  427. q-btn.acrylic-btn(
  428. flat
  429. icon='las la-arrow-circle-right'
  430. color='primary'
  431. @click='sendWelcomeEmail'
  432. :label='t(`common.actions.proceed`)'
  433. )
  434. q-separator.q-my-sm(inset)
  435. q-item
  436. blueprint-icon(icon='apply', :hue-rotate='45')
  437. q-item-section
  438. q-item-label {{state.user.isVerified ? t(`admin.users.unverify`) : t(`admin.users.verify`)}}
  439. q-item-label(caption) {{state.user.isVerified ? t(`admin.users.unverifyHint`) : t(`admin.users.verifyHint`)}}
  440. q-item-label(caption): strong(:class='state.user.isVerified ? `text-positive` : `text-negative`') {{state.user.isVerified ? t(`admin.users.verified`) : t(`admin.users.unverified`)}}
  441. q-item-section(side)
  442. q-btn.acrylic-btn(
  443. flat
  444. icon='las la-arrow-circle-right'
  445. color='primary'
  446. @click='toggleVerified'
  447. :label='t(`common.actions.proceed`)'
  448. )
  449. q-separator.q-my-sm(inset)
  450. q-item
  451. blueprint-icon(icon='unfriend', :hue-rotate='45')
  452. q-item-section
  453. q-item-label {{state.user.isActive ? t(`admin.users.ban`) : t(`admin.users.unban`)}}
  454. q-item-label(caption) {{state.user.isActive ? t(`admin.users.banHint`) : t(`admin.users.unbanHint`)}}
  455. q-item-label(caption): strong(:class='state.user.isActive ? `text-positive` : `text-negative`') {{state.user.isActive ? t(`admin.users.active`) : t(`admin.users.banned`)}}
  456. q-item-section(side)
  457. q-btn.acrylic-btn(
  458. flat
  459. icon='las la-arrow-circle-right'
  460. color='primary'
  461. @click='toggleBan'
  462. :label='t(`common.actions.proceed`)'
  463. )
  464. q-card.shadow-1.q-py-sm.q-mt-md
  465. q-item
  466. blueprint-icon(icon='denied', :hue-rotate='140')
  467. q-item-section
  468. q-item-label {{t(`admin.users.delete`)}}
  469. q-item-label(caption) {{t(`admin.users.deleteHint`)}}
  470. q-item-section(side)
  471. q-btn.acrylic-btn(
  472. flat
  473. icon='las la-arrow-circle-right'
  474. color='negative'
  475. @click='deleteUser'
  476. :label='t(`common.actions.proceed`)'
  477. )
  478. </template>
  479. <script setup>
  480. import gql from 'graphql-tag'
  481. import cloneDeep from 'lodash/cloneDeep'
  482. import some from 'lodash/some'
  483. import find from 'lodash/find'
  484. import findKey from 'lodash/findKey'
  485. import _get from 'lodash/get'
  486. import map from 'lodash/map'
  487. import { DateTime } from 'luxon'
  488. import { useI18n } from 'vue-i18n'
  489. import { useQuasar } from 'quasar'
  490. import { computed, onMounted, reactive, watch } from 'vue'
  491. import { useRouter, useRoute } from 'vue-router'
  492. import { useAdminStore } from 'src/stores/admin'
  493. import { useDataStore } from 'src/stores/data'
  494. import UserChangePwdDialog from './UserChangePwdDialog.vue'
  495. import UtilCodeEditor from './UtilCodeEditor.vue'
  496. // QUASAR
  497. const $q = useQuasar()
  498. // STORES
  499. const adminStore = useAdminStore()
  500. const dataStore = useDataStore()
  501. // ROUTER
  502. const router = useRouter()
  503. const route = useRoute()
  504. // I18N
  505. const { t } = useI18n()
  506. // DATA
  507. const state = reactive({
  508. invalidCharsRegex: /^[^<>"]+$/,
  509. user: {
  510. meta: {},
  511. prefs: {},
  512. groups: []
  513. },
  514. groups: [],
  515. groupToAdd: null,
  516. loading: 0,
  517. metadataInvalidJSON: false
  518. })
  519. const sections = [
  520. { key: 'overview', text: t('admin.users.overview'), icon: 'las la-user' },
  521. { key: 'activity', text: t('admin.users.activity'), icon: 'las la-chart-area' },
  522. { key: 'auth', text: t('admin.users.auth'), icon: 'las la-key' },
  523. { key: 'groups', text: t('admin.users.groups'), icon: 'las la-users' },
  524. { key: 'metadata', text: t('admin.users.metadata'), icon: 'las la-clipboard-list' },
  525. { key: 'operations', text: t('admin.users.operations'), icon: 'las la-tools' }
  526. ]
  527. // COMPUTED
  528. const metadata = computed({
  529. get () { return JSON.stringify(state.user.meta ?? {}, null, 2) },
  530. set (val) {
  531. try {
  532. state.user.meta = JSON.parse(val)
  533. state.metadataInvalidJSON = false
  534. } catch (err) {
  535. state.metadataInvalidJSON = true
  536. }
  537. }
  538. })
  539. const localAuthId = computed(() => {
  540. return findKey(state.user.auth, ['module', 'local'])
  541. })
  542. const localAuth = computed({
  543. get () {
  544. return localAuthId.value ? _get(state.user.auth, localAuthId.value, {}) : {}
  545. },
  546. set (val) {
  547. if (localAuthId.value) {
  548. state.user.auth[localAuthId.value] = val
  549. }
  550. }
  551. })
  552. const linkedAuthProviders = computed(() => {
  553. if (!state.user?.auth) { return [] }
  554. return map(state.user.auth, (obj, key) => {
  555. return {
  556. ...obj,
  557. _id: key
  558. }
  559. }).filter(prv => prv.module !== 'local')
  560. })
  561. // WATCHERS
  562. watch(() => route.params.section, checkRoute)
  563. // METHODS
  564. async function fetchUser () {
  565. state.loading++
  566. $q.loading.show()
  567. try {
  568. const resp = await APOLLO_CLIENT.query({
  569. query: gql`
  570. query adminFetchUser (
  571. $id: UUID!
  572. ) {
  573. groups {
  574. id
  575. name
  576. }
  577. userById(
  578. id: $id
  579. ) {
  580. id
  581. email
  582. name
  583. isSystem
  584. isVerified
  585. isActive
  586. auth
  587. meta
  588. prefs
  589. lastLoginAt
  590. createdAt
  591. updatedAt
  592. groups {
  593. id
  594. name
  595. }
  596. }
  597. }
  598. `,
  599. variables: {
  600. id: adminStore.overlayOpts.id
  601. },
  602. fetchPolicy: 'network-only'
  603. })
  604. state.groups = resp?.data?.groups?.filter(g => g.id !== '10000000-0000-4000-0000-000000000001') ?? []
  605. if (resp?.data?.userById) {
  606. state.user = cloneDeep(resp.data.userById)
  607. } else {
  608. throw new Error('An unexpected error occured while fetching user details.')
  609. }
  610. } catch (err) {
  611. $q.notify({
  612. type: 'negative',
  613. message: err.message
  614. })
  615. }
  616. $q.loading.hide()
  617. state.loading--
  618. }
  619. function close () {
  620. adminStore.$patch({ overlay: '' })
  621. }
  622. function checkRoute () {
  623. if (!route.params.section) {
  624. router.replace({ params: { section: 'overview' } })
  625. }
  626. if (route.params.section === 'metadata') {
  627. state.metadataInvalidJSON = false
  628. }
  629. }
  630. function humanizeDate (val) {
  631. if (!val) { return '---' }
  632. return DateTime.fromISO(val).toLocaleString(DateTime.DATETIME_FULL)
  633. }
  634. function assignGroup () {
  635. if (!state.groupToAdd) {
  636. $q.notify({
  637. type: 'negative',
  638. message: t('admin.users.noGroupSelected')
  639. })
  640. } else if (some(state.user.groups, gr => gr.id === state.groupToAdd)) {
  641. $q.notify({
  642. type: 'warning',
  643. message: t('admin.users.groupAlreadyAssigned')
  644. })
  645. } else {
  646. const newGroup = find(state.groups, ['id', state.groupToAdd])
  647. state.user.groups = [...state.user.groups, newGroup]
  648. }
  649. }
  650. function unassignGroup (id) {
  651. if (state.user.groups.length <= 1) {
  652. $q.notify({
  653. type: 'negative',
  654. message: t('admin.users.minimumGroupRequired')
  655. })
  656. } else {
  657. state.user.groups = state.user.groups.filter(gr => gr.id === id)
  658. }
  659. }
  660. async function save (patch, { silent, keepOpen } = { silent: false, keepOpen: false }) {
  661. $q.loading.show()
  662. if (!patch) {
  663. patch = {
  664. name: state.user.name,
  665. email: state.user.email,
  666. isVerified: state.user.isVerified,
  667. isActive: state.user.isActive,
  668. meta: state.user.meta,
  669. prefs: state.user.prefs,
  670. groups: state.user.groups.map(gr => gr.id)
  671. }
  672. }
  673. try {
  674. const resp = await APOLLO_CLIENT.mutate({
  675. mutation: gql`
  676. mutation adminSaveUser (
  677. $id: UUID!
  678. $patch: UserUpdateInput!
  679. ) {
  680. updateUser (
  681. id: $id
  682. patch: $patch
  683. ) {
  684. operation {
  685. succeeded
  686. message
  687. }
  688. }
  689. }
  690. `,
  691. variables: {
  692. id: adminStore.overlayOpts.id,
  693. patch
  694. }
  695. })
  696. if (resp?.data?.updateUser?.operation?.succeeded) {
  697. if (!silent) {
  698. $q.notify({
  699. type: 'positive',
  700. message: t('admin.users.saveSuccess')
  701. })
  702. }
  703. if (!keepOpen) {
  704. close()
  705. }
  706. } else {
  707. throw new Error(resp?.data?.updateUser?.operation?.message || 'An unexpected error occured.')
  708. }
  709. } catch (err) {
  710. $q.notify({
  711. type: 'negative',
  712. message: err.message
  713. })
  714. }
  715. $q.loading.hide()
  716. }
  717. function changePassword () {
  718. $q.dialog({
  719. component: UserChangePwdDialog,
  720. componentProps: {
  721. userId: adminStore.overlayOpts.id
  722. }
  723. }).onOk(({ mustChangePassword }) => {
  724. localAuth.value = {
  725. ...localAuth.value,
  726. mustChangePwd: mustChangePassword
  727. }
  728. })
  729. }
  730. function invalidateTFA () {
  731. $q.dialog({
  732. title: t('admin.users.tfaInvalidate'),
  733. message: t('admin.users.tfaInvalidateConfirm'),
  734. cancel: true,
  735. persistent: true,
  736. ok: {
  737. label: t('common.actions.confirm')
  738. }
  739. }).onOk(() => {
  740. localAuth.value.tfaSecret = ''
  741. $q.notify({
  742. type: 'positive',
  743. message: t('admin.users.tfaInvalidateSuccess')
  744. })
  745. })
  746. }
  747. async function sendWelcomeEmail () {
  748. }
  749. function toggleVerified () {
  750. state.user.isVerified = !state.user.isVerified
  751. save({
  752. isVerified: state.user.isVerified
  753. }, { silent: true, keepOpen: true })
  754. }
  755. function toggleBan () {
  756. state.user.isActive = !state.user.isActive
  757. save({
  758. isActive: state.user.isActive
  759. }, { silent: true, keepOpen: true })
  760. }
  761. async function deleteUser () {
  762. }
  763. // MOUNTED
  764. onMounted(() => {
  765. checkRoute()
  766. fetchUser()
  767. })
  768. </script>
  769. <style lang="scss" scoped>
  770. .metadata-codemirror {
  771. &:deep(.cm-editor) {
  772. min-height: 150px;
  773. border-radius: 5px;
  774. border: 1px solid #CCC;
  775. }
  776. }
  777. </style>