UserEditOverlay.vue 28 KB

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