UserCreateDialog.vue 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. <template lang="pug">
  2. q-dialog(ref='dialogRef', @hide='onDialogHide')
  3. q-card(style='min-width: 650px;')
  4. q-card-section.card-header
  5. q-icon(name='img:/_assets/icons/fluent-plus-plus.svg', left, size='sm')
  6. span {{t(`admin.users.create`)}}
  7. q-form.q-py-sm(ref='createUserForm', @submit='create')
  8. q-item
  9. blueprint-icon(icon='person')
  10. q-item-section
  11. q-input(
  12. outlined
  13. v-model='state.userName'
  14. dense
  15. :rules='userNameValidation'
  16. hide-bottom-space
  17. :label='t(`common.field.name`)'
  18. :aria-label='t(`common.field.name`)'
  19. lazy-rules='ondemand'
  20. autofocus
  21. ref='iptName'
  22. )
  23. q-item
  24. blueprint-icon(icon='email')
  25. q-item-section
  26. q-input(
  27. outlined
  28. v-model='state.userEmail'
  29. dense
  30. type='email'
  31. :rules='userEmailValidation'
  32. hide-bottom-space
  33. :label='t(`admin.users.email`)'
  34. :aria-label='t(`admin.users.email`)'
  35. lazy-rules='ondemand'
  36. autofocus
  37. )
  38. q-item
  39. blueprint-icon(icon='password')
  40. q-item-section
  41. q-input(
  42. outlined
  43. v-model='state.userPassword'
  44. dense
  45. :rules='userPasswordValidation'
  46. hide-bottom-space
  47. :label='t(`admin.users.password`)'
  48. :aria-label='t(`admin.users.password`)'
  49. lazy-rules='ondemand'
  50. autofocus
  51. )
  52. template(#append)
  53. .flex.items-center
  54. q-badge(
  55. :color='passwordStrength.color'
  56. :label='passwordStrength.label'
  57. )
  58. q-separator.q-mx-sm(vertical)
  59. q-btn(
  60. flat
  61. dense
  62. padding='none xs'
  63. color='brown'
  64. @click='randomizePassword'
  65. )
  66. q-icon(name='las la-dice-d6')
  67. .q-pl-xs.text-caption: strong Generate
  68. q-item
  69. blueprint-icon(icon='team')
  70. q-item-section
  71. q-select(
  72. outlined
  73. :options='state.groups'
  74. v-model='state.userGroups'
  75. multiple
  76. map-options
  77. emit-value
  78. option-value='id'
  79. option-label='name'
  80. options-dense
  81. dense
  82. :rules='userGroupsValidation'
  83. hide-bottom-space
  84. :label='t(`admin.users.groups`)'
  85. :aria-label='t(`admin.users.groups`)'
  86. lazy-rules='ondemand'
  87. :loading='state.loadingGroups'
  88. )
  89. template(v-slot:selected)
  90. .text-caption(v-if='state.userGroups.length > 1')
  91. i18n-t(keypath='admin.users.groupsSelected')
  92. template(#count)
  93. strong {{ state.userGroups.length }}
  94. .text-caption(v-else-if='state.userGroups.length === 1')
  95. i18n-t(keypath='admin.users.groupSelected')
  96. template(#group)
  97. strong {{ selectedGroupName }}
  98. span(v-else)
  99. template(v-slot:option='{ itemProps, opt, selected, toggleOption }')
  100. q-item(
  101. v-bind='itemProps'
  102. )
  103. q-item-section(side)
  104. q-checkbox(
  105. size='sm'
  106. :model-value='selected'
  107. @update:model-value='toggleOption(opt)'
  108. )
  109. q-item-section
  110. q-item-label {{opt.name}}
  111. q-item(tag='label', v-ripple)
  112. blueprint-icon(icon='password-reset')
  113. q-item-section
  114. q-item-label {{t(`admin.users.mustChangePwd`)}}
  115. q-item-label(caption) {{t(`admin.users.mustChangePwdHint`)}}
  116. q-item-section(avatar)
  117. q-toggle(
  118. v-model='state.userMustChangePassword'
  119. color='primary'
  120. checked-icon='las la-check'
  121. unchecked-icon='las la-times'
  122. :aria-label='t(`admin.users.mustChangePwd`)'
  123. )
  124. q-item(tag='label', v-ripple)
  125. blueprint-icon(icon='email-open')
  126. q-item-section
  127. q-item-label {{t(`admin.users.sendWelcomeEmail`)}}
  128. q-item-label(caption) {{t(`admin.users.sendWelcomeEmailHint`)}}
  129. q-item-section(avatar)
  130. q-toggle(
  131. v-model='state.userSendWelcomeEmail'
  132. color='primary'
  133. checked-icon='las la-check'
  134. unchecked-icon='las la-times'
  135. :aria-label='t(`admin.users.sendWelcomeEmail`)'
  136. )
  137. q-card-actions.card-actions
  138. q-checkbox(
  139. v-model='state.keepOpened'
  140. color='primary'
  141. :label='t(`admin.users.createKeepOpened`)'
  142. size='sm'
  143. )
  144. q-space
  145. q-btn.acrylic-btn(
  146. flat
  147. :label='t(`common.actions.cancel`)'
  148. color='grey'
  149. padding='xs md'
  150. @click='onDialogCancel'
  151. )
  152. q-btn(
  153. unelevated
  154. :label='t(`common.actions.create`)'
  155. color='primary'
  156. padding='xs md'
  157. @click='create'
  158. :loading='state.loading > 0'
  159. )
  160. </template>
  161. <script setup>
  162. import gql from 'graphql-tag'
  163. import sampleSize from 'lodash/sampleSize'
  164. import zxcvbn from 'zxcvbn'
  165. import cloneDeep from 'lodash/cloneDeep'
  166. import { useI18n } from 'vue-i18n'
  167. import { useDialogPluginComponent, useQuasar } from 'quasar'
  168. import { computed, onMounted, reactive, ref } from 'vue'
  169. // EMITS
  170. defineEmits([
  171. ...useDialogPluginComponent.emits
  172. ])
  173. // QUASAR
  174. const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
  175. const $q = useQuasar()
  176. // I18N
  177. const { t } = useI18n()
  178. // DATA
  179. const state = reactive({
  180. userName: '',
  181. userEmail: '',
  182. userPassword: '',
  183. userGroups: [],
  184. userMustChangePassword: false,
  185. userSendWelcomeEmail: false,
  186. keepOpened: false,
  187. groups: [],
  188. loadingGroups: false,
  189. loading: false
  190. })
  191. // REFS
  192. const createUserForm = ref(null)
  193. const iptName = ref(null)
  194. // COMPUTED
  195. const passwordStrength = computed(() => {
  196. if (state.userPassword.length < 8) {
  197. return {
  198. color: 'negative',
  199. label: t('admin.users.pwdStrengthWeak')
  200. }
  201. } else {
  202. switch (zxcvbn(state.userPassword).score) {
  203. case 1:
  204. return {
  205. color: 'deep-orange-7',
  206. label: t('admin.users.pwdStrengthPoor')
  207. }
  208. case 2:
  209. return {
  210. color: 'purple-7',
  211. label: t('admin.users.pwdStrengthMedium')
  212. }
  213. case 3:
  214. return {
  215. color: 'blue-7',
  216. label: t('admin.users.pwdStrengthGood')
  217. }
  218. case 4:
  219. return {
  220. color: 'green-7',
  221. label: t('admin.users.pwdStrengthStrong')
  222. }
  223. default:
  224. return {
  225. color: 'negative',
  226. label: t('admin.users.pwdStrengthWeak')
  227. }
  228. }
  229. }
  230. })
  231. const selectedGroupName = computed(() => {
  232. return state.groups.filter(g => g.id === state.userGroups[0])[0]?.name
  233. })
  234. // VALIDATION RULES
  235. const userNameValidation = [
  236. val => val.length > 0 || t('admin.users.nameMissing'),
  237. val => /^[^<>"]+$/.test(val) || t('admin.users.nameInvalidChars')
  238. ]
  239. const userEmailValidation = [
  240. val => val.length > 0 || t('admin.users.emailMissing'),
  241. val => /^.+@.+\..+$/.test(val) || t('admin.users.emailInvalid')
  242. ]
  243. const userPasswordValidation = [
  244. val => val.length > 0 || t('admin.users.passwordMissing'),
  245. val => val.length >= 8 || t('admin.users.passwordTooShort')
  246. ]
  247. const userGroupsValidation = [
  248. val => val.length > 0 || t('admin.users.groupsMissing')
  249. ]
  250. // METHODS
  251. async function loadGroups () {
  252. state.loading++
  253. state.loadingGroups = true
  254. const resp = await APOLLO_CLIENT.query({
  255. query: gql`
  256. query getGroupsForCreateUser {
  257. groups {
  258. id
  259. name
  260. }
  261. }
  262. `,
  263. fetchPolicy: 'network-only'
  264. })
  265. state.groups = cloneDeep(resp?.data?.groups?.filter(g => g.id !== '10000000-0000-4000-8000-000000000001') ?? [])
  266. state.loadingGroups = false
  267. state.loading--
  268. }
  269. function randomizePassword () {
  270. const pwdChars = 'abcdefghkmnpqrstuvwxyzABCDEFHJKLMNPQRSTUVWXYZ23456789_*=?#!()+'
  271. state.userPassword = sampleSize(pwdChars, 16).join('')
  272. }
  273. async function create () {
  274. state.loading++
  275. try {
  276. const isFormValid = await createUserForm.value.validate(true)
  277. if (!isFormValid) {
  278. throw new Error(t('admin.users.createInvalidData'))
  279. }
  280. const resp = await APOLLO_CLIENT.mutate({
  281. mutation: gql`
  282. mutation createUser (
  283. $name: String!
  284. $email: String!
  285. $password: String!
  286. $groups: [UUID]!
  287. $mustChangePassword: Boolean!
  288. $sendWelcomeEmail: Boolean!
  289. ) {
  290. createUser (
  291. name: $name
  292. email: $email
  293. password: $password
  294. groups: $groups
  295. mustChangePassword: $mustChangePassword
  296. sendWelcomeEmail: $sendWelcomeEmail
  297. ) {
  298. operation {
  299. succeeded
  300. message
  301. }
  302. }
  303. }
  304. `,
  305. variables: {
  306. name: state.userName,
  307. email: state.userEmail,
  308. password: state.userPassword,
  309. groups: state.userGroups,
  310. mustChangePassword: state.userMustChangePassword,
  311. sendWelcomeEmail: state.userSendWelcomeEmail
  312. }
  313. })
  314. if (resp?.data?.createUser?.operation?.succeeded) {
  315. $q.notify({
  316. type: 'positive',
  317. message: t('admin.users.createSuccess')
  318. })
  319. if (state.keepOpened) {
  320. state.userName = ''
  321. state.userEmail = ''
  322. state.userPassword = ''
  323. iptName.value.focus()
  324. } else {
  325. onDialogOK()
  326. }
  327. } else {
  328. throw new Error(resp?.data?.createUser?.operation?.message || 'An unexpected error occured.')
  329. }
  330. } catch (err) {
  331. $q.notify({
  332. type: 'negative',
  333. message: err.message
  334. })
  335. }
  336. state.loading--
  337. }
  338. // MOUNTED
  339. onMounted(loadGroups)
  340. </script>