GroupEditOverlay.vue 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189
  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-people.svg', left, size='md')
  5. div
  6. span {{t(`admin.groups.edit`)}}
  7. .text-caption {{state.group.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='refresh'
  17. )
  18. q-tooltip(anchor='center left', self='center right') {{t(`common.actions.refresh`)}}
  19. q-btn(
  20. push
  21. color='white'
  22. text-color='grey-7'
  23. :label='t(`common.actions.close`)'
  24. icon='las la-times'
  25. @click='close'
  26. )
  27. q-btn(
  28. push
  29. color='positive'
  30. text-color='white'
  31. :label='t(`common.actions.save`)'
  32. icon='las la-check'
  33. )
  34. q-drawer.bg-dark-6(:model-value='true', :width='250', dark)
  35. q-list(padding, v-show='!state.isLoading')
  36. q-item(
  37. v-for='sc of sections'
  38. :key='`section-` + sc.key'
  39. clickable
  40. :to='{ params: { section: sc.key } }'
  41. active-class='bg-primary text-white'
  42. :disabled='sc.disabled'
  43. )
  44. q-item-section(side)
  45. q-icon(:name='sc.icon', color='white')
  46. q-item-section {{sc.text}}
  47. q-item-section(side, v-if='sc.usersTotal')
  48. q-badge(color='dark-3', :label='state.usersTotal')
  49. q-item-section(side, v-if='sc.rulesTotal && state.group.rules')
  50. q-badge(color='dark-3', :label='state.group.rules.length')
  51. q-page-container
  52. q-page(v-if='state.isLoading')
  53. //- -----------------------------------------------------------------------
  54. //- OVERVIEW
  55. //- -----------------------------------------------------------------------
  56. q-page(v-else-if='route.params.section === `overview`')
  57. .q-pa-md
  58. .row.q-col-gutter-md
  59. .col-12.col-lg-8
  60. q-card.shadow-1.q-pb-sm
  61. q-card-section
  62. .text-subtitle1 {{t('admin.groups.general')}}
  63. q-item
  64. blueprint-icon(icon='team')
  65. q-item-section
  66. q-item-label {{t(`admin.groups.name`)}}
  67. q-item-label(caption) {{t(`admin.groups.nameHint`)}}
  68. q-item-section
  69. q-input(
  70. outlined
  71. v-model='state.group.name'
  72. dense
  73. :rules='groupNameValidation'
  74. hide-bottom-space
  75. :aria-label='t(`admin.groups.name`)'
  76. )
  77. q-card.shadow-1.q-pb-sm.q-mt-md
  78. q-card-section
  79. .text-subtitle1 {{t('admin.groups.authBehaviors')}}
  80. q-item
  81. blueprint-icon(icon='double-right')
  82. q-item-section
  83. q-item-label {{t(`admin.groups.redirectOnLogin`)}}
  84. q-item-label(caption) {{t(`admin.groups.redirectOnLoginHint`)}}
  85. q-item-section
  86. q-input(
  87. outlined
  88. v-model='state.group.redirectOnLogin'
  89. dense
  90. :aria-label='t(`admin.groups.redirectOnLogin`)'
  91. )
  92. q-separator.q-my-sm(inset)
  93. q-item
  94. blueprint-icon(icon='chevron-right')
  95. q-item-section
  96. q-item-label {{t(`admin.groups.redirectOnFirstLogin`)}}
  97. q-item-label(caption) {{t(`admin.groups.redirectOnFirstLoginHint`)}}
  98. q-item-section
  99. q-input(
  100. outlined
  101. v-model='state.group.redirectOnFirstLogin'
  102. dense
  103. :aria-label='t(`admin.groups.redirectOnLogin`)'
  104. )
  105. q-separator.q-my-sm(inset)
  106. q-item
  107. blueprint-icon(icon='exit')
  108. q-item-section
  109. q-item-label {{t(`admin.groups.redirectOnLogout`)}}
  110. q-item-label(caption) {{t(`admin.groups.redirectOnLogoutHint`)}}
  111. q-item-section
  112. q-input(
  113. outlined
  114. v-model='state.group.redirectOnLogout'
  115. dense
  116. :aria-label='t(`admin.groups.redirectOnLogout`)'
  117. )
  118. .col-12.col-lg-4
  119. q-card.shadow-1.q-pb-sm
  120. q-card-section
  121. .text-subtitle1 {{t('admin.groups.info')}}
  122. q-item
  123. blueprint-icon(icon='team', :hue-rotate='-45')
  124. q-item-section
  125. q-item-label {{t(`common.field.id`)}}
  126. q-item-label: strong {{state.group.id}}
  127. q-separator.q-my-sm(inset)
  128. q-item
  129. blueprint-icon(icon='calendar-plus', :hue-rotate='-45')
  130. q-item-section
  131. q-item-label {{t(`common.field.createdOn`)}}
  132. q-item-label: strong {{humanizeDate(state.group.createdAt)}}
  133. q-separator.q-my-sm(inset)
  134. q-item
  135. blueprint-icon(icon='summertime', :hue-rotate='-45')
  136. q-item-section
  137. q-item-label {{t(`common.field.lastUpdated`)}}
  138. q-item-label: strong {{humanizeDate(state.group.updatedAt)}}
  139. //- -----------------------------------------------------------------------
  140. //- RULES
  141. //- -----------------------------------------------------------------------
  142. q-page(v-else-if='route.params.section === `rules`')
  143. q-toolbar.q-pl-md(
  144. :class='$q.dark.isActive ? `bg-dark-3` : `bg-white`'
  145. )
  146. .text-subtitle1 {{t('admin.groups.rules')}}
  147. q-space
  148. q-btn.acrylic-btn.q-mr-sm(
  149. icon='las la-question-circle'
  150. flat
  151. color='grey'
  152. type='a'
  153. href='https://docs.js.wiki/admin/groups#rules'
  154. target='_blank'
  155. )
  156. q-btn.acrylic-btn.q-mr-sm(
  157. flat
  158. color='indigo'
  159. icon='las la-file-export'
  160. @click='exportRules'
  161. )
  162. q-tooltip {{t('admin.groups.exportRules')}}
  163. q-btn.acrylic-btn.q-mr-sm(
  164. flat
  165. color='indigo'
  166. icon='las la-file-import'
  167. @click='importRules'
  168. )
  169. q-tooltip {{t('admin.groups.importRules')}}
  170. q-btn(
  171. unelevated
  172. color='primary'
  173. icon='las la-plus'
  174. label='New Rule'
  175. @click='newRule'
  176. )
  177. q-separator
  178. .q-pa-md
  179. q-banner(
  180. v-if='!state.group.rules || state.group.rules.length < 1'
  181. rounded
  182. :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-4 text-grey-9`'
  183. ) {{t('admin.groups.rulesNone')}}
  184. q-card.shadow-1.q-pb-sm(v-else)
  185. q-card-section
  186. .admin-groups-rule(
  187. v-for='(rule, idx) of state.group.rules'
  188. :key='rule.id'
  189. )
  190. .admin-groups-rule-icon(:class='getRuleModeColor(rule.mode)')
  191. q-icon.cursor-pointer(
  192. :name='getRuleModeIcon(rule.mode)'
  193. color='white'
  194. @click='rule.mode = getNextRuleMode(rule.mode)'
  195. )
  196. .admin-groups-rule-name
  197. .admin-groups-rule-name-text: strong(:class='getRuleModeColor(rule.mode)') {{getRuleModeName(rule.mode)}}
  198. q-separator.q-ml-sm.q-mr-xs(vertical)
  199. input(
  200. type='text'
  201. v-model='rule.name'
  202. placeholder='Rule Name'
  203. )
  204. q-card.admin-groups-rule-card.q-mt-md(flat)
  205. q-card-section.admin-groups-rule-card-permissions(:class='getRuleModeClass(rule.mode)')
  206. q-select.q-mt-xs(
  207. standout
  208. v-model='rule.roles'
  209. emit-value
  210. map-options
  211. dense
  212. :aria-label='t(`admin.groups.ruleSites`)'
  213. :options='rules'
  214. placeholder='Select permissions...'
  215. option-value='permission'
  216. option-label='title'
  217. options-dense
  218. multiple
  219. use-chips
  220. stack-label
  221. )
  222. template(v-slot:option='{ itemProps, itemEvents, opt, selected, toggleOption }')
  223. q-item(v-bind='itemProps', v-on='itemEvents')
  224. q-item-section(side)
  225. q-toggle(
  226. :value='selected'
  227. @input='toggleOption(opt)'
  228. color='primary'
  229. checked-icon='las la-check'
  230. unchecked-icon='las la-times'
  231. :aria-label='opt.label'
  232. )
  233. //- q-item-section(side, style='flex-basis: 150px;')
  234. //- q-chip.text-caption(
  235. //- square
  236. //- color='teal'
  237. //- text-color='white'
  238. //- dense
  239. //- ) {{opt.permission}}
  240. q-item-section
  241. q-item-label {{opt.title}}
  242. q-item-label(caption) {{opt.hint}}
  243. q-btn.acrylic-btn.q-ml-md(
  244. flat
  245. icon='las la-trash'
  246. color='negative'
  247. padding='sm sm'
  248. size='md',
  249. @click='deleteRule(rule.id)'
  250. )
  251. q-card-section(horizontal)
  252. q-card-section.admin-groups-rule-card-filters
  253. .text-caption Applies to...
  254. q-select.q-mt-xs(
  255. standout
  256. v-model='rule.sites'
  257. emit-value
  258. map-options
  259. dense
  260. :aria-label='t(`admin.groups.ruleSites`)'
  261. :options='adminStore.sites'
  262. option-value='id'
  263. option-label='title'
  264. multiple
  265. behavior='dialog'
  266. :display-value='t(`admin.groups.selectedSites`, rule.sites.length, { count: rule.sites.length })'
  267. )
  268. template(v-slot:option='{ itemProps, itemEvents, opt, selected, toggleOption }')
  269. q-item(v-bind='itemProps', v-on='itemEvents')
  270. q-item-section
  271. q-item-label {{opt.title}}
  272. q-item-section(side)
  273. q-toggle(
  274. :value='selected'
  275. @input='toggleOption(opt)'
  276. color='primary'
  277. checked-icon='las la-check'
  278. unchecked-icon='las la-times'
  279. :aria-label='opt.label'
  280. )
  281. q-select.q-mt-sm(
  282. standout
  283. v-model='rule.locales'
  284. emit-value
  285. map-options
  286. dense
  287. :aria-label='t(`admin.groups.ruleLocales`)'
  288. :options='adminStore.locales'
  289. option-value='code'
  290. option-label='name'
  291. multiple
  292. behavior='dialog'
  293. :display-value='t(`admin.groups.selectedLocales`, rule.locales.length, { count: rule.locales.length, locale: rule.locales.length === 1 ? rule.locales[0].toUpperCase() : `` })'
  294. )
  295. template(v-slot:option='{ itemProps, opt, selected, toggleOption }')
  296. q-item(v-bind='itemProps')
  297. q-item-section
  298. q-item-label {{opt.name}}
  299. q-item-section(side)
  300. q-toggle(
  301. :model-value='selected'
  302. @update:model-value='toggleOption(opt)'
  303. color='primary'
  304. checked-icon='las la-check'
  305. unchecked-icon='las la-times'
  306. :aria-label='opt.name'
  307. )
  308. q-card-section.admin-groups-rule-card-pattern
  309. .text-caption Pattern
  310. q-select.q-mt-xs(
  311. standout
  312. v-model='rule.match'
  313. emit-value
  314. map-options
  315. dense
  316. :aria-label='t(`admin.groups.ruleMatch`)'
  317. :options=`[
  318. { label: t('admin.groups.ruleMatchStart'), value: 'START' },
  319. { label: t('admin.groups.ruleMatchEnd'), value: 'END' },
  320. { label: t('admin.groups.ruleMatchRegex'), value: 'REGEX' },
  321. { label: t('admin.groups.ruleMatchTag'), value: 'TAG' },
  322. { label: t('admin.groups.ruleMatchTagAll'), value: 'TAGALL' },
  323. { label: t('admin.groups.ruleMatchExact'), value: 'EXACT' }
  324. ]`
  325. )
  326. q-input.q-mt-sm(
  327. standout
  328. v-model='rule.path'
  329. dense
  330. :prefix='[`START`, `REGEX`, `EXACT`].includes(rule.match) ? `/` : null'
  331. :suffix='rule.match === `REGEX` ? `/` : null'
  332. :aria-label='t(`admin.groups.rulePath`)'
  333. )
  334. //- -----------------------------------------------------------------------
  335. //- PERMISSIONS
  336. //- -----------------------------------------------------------------------
  337. q-page(v-else-if='route.params.section === `permissions`')
  338. .q-pa-md
  339. .row.q-col-gutter-md
  340. .col-12.col-lg-6
  341. q-card.shadow-1.q-pb-sm
  342. .flex.justify-between
  343. q-card-section
  344. .text-subtitle1 {{t(`admin.groups.permissions`)}}
  345. q-card-section
  346. q-btn.acrylic-btn(
  347. icon='las la-question-circle'
  348. flat
  349. color='grey'
  350. type='a'
  351. href='https://docs.js.wiki/admin/groups#permissions'
  352. target='_blank'
  353. )
  354. template(v-for='(perm, idx) of permissions', :key='perm.permission')
  355. q-item(tag='label', v-ripple)
  356. q-item-section.items-center(style='flex: 0 0 40px;')
  357. q-icon(
  358. name='las la-comments'
  359. color='primary'
  360. size='sm'
  361. )
  362. q-item-section
  363. q-item-label {{perm.permission}}
  364. q-item-label(caption) {{perm.hint}}
  365. q-item-section(avatar)
  366. q-toggle(
  367. v-model='state.group.permissions'
  368. :val='perm.permission'
  369. color='primary'
  370. checked-icon='las la-check'
  371. unchecked-icon='las la-times'
  372. :aria-label='t(`admin.general.allowComments`)'
  373. )
  374. q-separator.q-my-sm(inset, v-if='idx < permissions.length - 1')
  375. //- -----------------------------------------------------------------------
  376. //- USERS
  377. //- -----------------------------------------------------------------------
  378. q-page(v-else-if='route.params.section === `users`')
  379. q-toolbar(
  380. :class='$q.dark.isActive ? `bg-dark-3` : `bg-white`'
  381. )
  382. .text-subtitle1 {{t('admin.groups.users')}}
  383. q-space
  384. q-btn.acrylic-btn.q-mr-sm(
  385. icon='las la-question-circle'
  386. flat
  387. color='grey'
  388. type='a'
  389. href='https://docs.js.wiki/admin/groups#users'
  390. target='_blank'
  391. )
  392. q-input.denser.fill-outline.q-mr-sm(
  393. outlined
  394. v-model='state.usersFilter'
  395. :placeholder='t(`admin.groups.filterUsers`)'
  396. dense
  397. )
  398. template(#prepend)
  399. q-icon(name='las la-search')
  400. q-btn.q-mr-sm.acrylic-btn(
  401. icon='las la-redo-alt'
  402. flat
  403. color='secondary'
  404. @click='refreshUsers'
  405. )
  406. q-btn.q-mr-xs(
  407. unelevated
  408. icon='las la-user-plus'
  409. :label='t(`admin.groups.assignUser`)'
  410. color='primary'
  411. @click='assignUser'
  412. )
  413. q-separator
  414. .q-pa-md
  415. q-banner(
  416. v-if='!state.users || state.users.length < 1'
  417. rounded
  418. :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-4 text-grey-9`'
  419. ) {{t('admin.groups.usersNone')}}
  420. q-card.shadow-1
  421. q-table(
  422. :rows='state.users'
  423. :columns='usersHeaders'
  424. row-key='id'
  425. flat
  426. hide-header
  427. hide-bottom
  428. :rows-per-page-options='[0]'
  429. :loading='state.isLoadingUsers'
  430. )
  431. template(v-slot:body-cell-id='props')
  432. q-td(:props='props')
  433. q-icon(name='las la-user', color='primary', size='sm')
  434. template(v-slot:body-cell-name='props')
  435. q-td(:props='props')
  436. .flex.items-center
  437. strong {{props.value}}
  438. q-icon.q-ml-sm(
  439. v-if='props.row.isSystem'
  440. name='las la-lock'
  441. color='pink'
  442. )
  443. q-icon.q-ml-sm(
  444. v-if='!props.row.isActive'
  445. name='las la-ban'
  446. color='pink'
  447. )
  448. template(v-slot:body-cell-email='props')
  449. q-td(:props='props')
  450. em {{ props.value }}
  451. template(v-slot:body-cell-date='props')
  452. q-td(:props='props')
  453. i18n-t.text-caption(keypath='admin.users.createdAt', tag='div')
  454. template(#date)
  455. strong {{ humanizeDate(props.value) }}
  456. i18n-t.text-caption(
  457. v-if='props.row.lastLoginAt'
  458. keypath='admin.users.lastLoginAt'
  459. tag='div'
  460. )
  461. template(#date)
  462. strong {{ humanizeDate(props.row.lastLoginAt) }}
  463. template(v-slot:body-cell-edit='props')
  464. q-td(:props='props')
  465. q-btn.acrylic-btn.q-mr-sm(
  466. v-if='!props.row.isSystem'
  467. flat
  468. :to='`/_admin/users/` + props.row.id'
  469. icon='las la-pen'
  470. color='indigo'
  471. :label='t(`common.actions.edit`)'
  472. no-caps
  473. )
  474. q-btn.acrylic-btn(
  475. v-if='!props.row.isSystem'
  476. flat
  477. icon='las la-user-minus'
  478. color='accent'
  479. @click='unassignUser(props.row)'
  480. )
  481. .flex.flex-center.q-mt-md(v-if='usersTotalPages > 1')
  482. q-pagination(
  483. v-model='state.usersPage'
  484. :max='usersTotalPages'
  485. :max-pages='9'
  486. boundary-numbers
  487. direction-links
  488. )
  489. </template>
  490. <script setup>
  491. import gql from 'graphql-tag'
  492. import { DateTime } from 'luxon'
  493. import cloneDeep from 'lodash/cloneDeep'
  494. import some from 'lodash/some'
  495. import { v4 as uuid } from 'uuid'
  496. import { fileOpen } from 'browser-fs-access'
  497. import { useI18n } from 'vue-i18n'
  498. import { exportFile, useQuasar } from 'quasar'
  499. import { computed, onBeforeUnmount, onMounted, reactive, watch } from 'vue'
  500. import { useRouter, useRoute } from 'vue-router'
  501. import { useAdminStore } from 'src/stores/admin'
  502. // QUASAR
  503. const $q = useQuasar()
  504. // STORES
  505. const adminStore = useAdminStore()
  506. // ROUTER
  507. const router = useRouter()
  508. const route = useRoute()
  509. // I18N
  510. const { t } = useI18n()
  511. // DATA
  512. const state = reactive({
  513. group: {
  514. rules: []
  515. },
  516. isLoading: false,
  517. users: [],
  518. isLoadingUsers: false,
  519. usersFilter: '',
  520. usersPage: 1,
  521. usersPageSize: 15,
  522. usersTotal: 0
  523. })
  524. const sections = [
  525. { key: 'overview', text: t('admin.groups.overview'), icon: 'las la-users' },
  526. { key: 'rules', text: t('admin.groups.rules'), icon: 'las la-file-invoice', rulesTotal: true },
  527. { key: 'permissions', text: t('admin.groups.permissions'), icon: 'las la-list-alt' },
  528. { key: 'users', text: t('admin.groups.users'), icon: 'las la-user', usersTotal: true }
  529. ]
  530. const usersHeaders = [
  531. {
  532. align: 'center',
  533. field: 'id',
  534. name: 'id',
  535. sortable: false,
  536. style: 'width: 20px'
  537. },
  538. {
  539. label: t('common.field.name'),
  540. align: 'left',
  541. field: 'name',
  542. name: 'name',
  543. sortable: true
  544. },
  545. {
  546. label: t('admin.users.email'),
  547. align: 'left',
  548. field: 'email',
  549. name: 'email',
  550. sortable: false
  551. },
  552. {
  553. align: 'left',
  554. field: 'createdAt',
  555. name: 'date',
  556. sortable: false
  557. },
  558. {
  559. label: '',
  560. align: 'right',
  561. field: 'edit',
  562. name: 'edit',
  563. sortable: false,
  564. style: 'width: 250px'
  565. }
  566. ]
  567. const permissions = [
  568. {
  569. permission: 'write:users',
  570. hint: 'Can create or authorize new users, but not modify existing ones',
  571. warning: false,
  572. restrictedForSystem: true,
  573. disabled: false
  574. },
  575. {
  576. permission: 'manage:users',
  577. hint: 'Can manage all users (but not users with administrative permissions)',
  578. warning: false,
  579. restrictedForSystem: true,
  580. disabled: false
  581. },
  582. {
  583. permission: 'write:groups',
  584. hint: 'Can manage groups and assign CONTENT permissions / page rules',
  585. warning: false,
  586. restrictedForSystem: true,
  587. disabled: false
  588. },
  589. {
  590. permission: 'manage:groups',
  591. hint: 'Can manage groups and assign ANY permissions (but not manage:system) / page rules',
  592. warning: true,
  593. restrictedForSystem: true,
  594. disabled: false
  595. },
  596. {
  597. permission: 'manage:navigation',
  598. hint: 'Can manage the site navigation',
  599. warning: false,
  600. restrictedForSystem: true,
  601. disabled: false
  602. },
  603. {
  604. permission: 'manage:theme',
  605. hint: 'Can manage and modify themes',
  606. warning: false,
  607. restrictedForSystem: true,
  608. disabled: false
  609. },
  610. {
  611. permission: 'manage:api',
  612. hint: 'Can generate and revoke API keys',
  613. warning: true,
  614. restrictedForSystem: true,
  615. disabled: false
  616. },
  617. {
  618. permission: 'manage:system',
  619. hint: 'Can manage and access everything. Root administrator.',
  620. warning: true,
  621. restrictedForSystem: true,
  622. disabled: true
  623. }
  624. ]
  625. const rules = [
  626. {
  627. permission: 'read:pages',
  628. title: 'Read Pages',
  629. hint: 'Can view and search pages.',
  630. warning: false,
  631. restrictedForSystem: false,
  632. disabled: false
  633. },
  634. {
  635. permission: 'write:pages',
  636. title: 'Write Pages',
  637. hint: 'Can create and edit pages.',
  638. warning: false,
  639. restrictedForSystem: true,
  640. disabled: false
  641. },
  642. {
  643. permission: 'review:pages',
  644. title: 'Review Pages',
  645. hint: 'Can review and approve edits submitted by users.',
  646. warning: false,
  647. restrictedForSystem: true,
  648. disabled: false
  649. },
  650. {
  651. permission: 'manage:pages',
  652. title: 'Manage Pages',
  653. hint: 'Can move existing pages to other locations the user has write access to.',
  654. warning: false,
  655. restrictedForSystem: true,
  656. disabled: false
  657. },
  658. {
  659. permission: 'delete:pages',
  660. title: 'Delete Pages',
  661. hint: 'Can delete existing pages.',
  662. warning: false,
  663. restrictedForSystem: true,
  664. disabled: false
  665. },
  666. {
  667. permission: 'write:styles',
  668. title: 'Use CSS',
  669. hint: 'Can insert CSS styles in pages.',
  670. warning: false,
  671. restrictedForSystem: true,
  672. disabled: false
  673. },
  674. {
  675. permission: 'write:scripts',
  676. title: 'Use JavaScript',
  677. hint: 'Can insert JavaScript in pages.',
  678. warning: false,
  679. restrictedForSystem: true,
  680. disabled: false
  681. },
  682. {
  683. permission: 'read:source',
  684. title: 'View Pages Source',
  685. hint: 'Can view pages source.',
  686. warning: false,
  687. restrictedForSystem: false,
  688. disabled: false
  689. },
  690. {
  691. permission: 'read:history',
  692. title: 'View Page History',
  693. hint: 'Can view previous versions of pages.',
  694. warning: false,
  695. restrictedForSystem: false,
  696. disabled: false
  697. },
  698. {
  699. permission: 'read:assets',
  700. title: 'View Assets',
  701. hint: 'Can view / use assets (such as images and files) in pages.',
  702. warning: false,
  703. restrictedForSystem: false,
  704. disabled: false
  705. },
  706. {
  707. permission: 'write:assets',
  708. title: 'Upload Assets',
  709. hint: 'Can upload new assets (such as images and files).',
  710. warning: false,
  711. restrictedForSystem: true,
  712. disabled: false
  713. },
  714. {
  715. permission: 'manage:assets',
  716. title: 'Manage Assets',
  717. hint: 'Can edit and delete existing assets (such as images and files).',
  718. warning: false,
  719. restrictedForSystem: true,
  720. disabled: false
  721. },
  722. {
  723. permission: 'read:comments',
  724. title: 'Read Comments',
  725. hint: 'Can view page comments.',
  726. warning: false,
  727. restrictedForSystem: false,
  728. disabled: false
  729. },
  730. {
  731. permission: 'write:comments',
  732. title: 'Write Comments',
  733. hint: 'Can post new comments on pages.',
  734. warning: false,
  735. restrictedForSystem: false,
  736. disabled: false
  737. },
  738. {
  739. permission: 'manage:comments',
  740. title: 'Manage Comments',
  741. hint: 'Can edit and delete existing page comments.',
  742. warning: false,
  743. restrictedForSystem: true,
  744. disabled: false
  745. }
  746. ]
  747. // VALIDATION RULES
  748. const groupNameValidation = [
  749. val => /^[^<>"]+$/.test(val) || t('admin.groups.nameInvalidChars')
  750. ]
  751. // COMPUTED
  752. const usersTotalPages = computed(() => {
  753. if (state.usersTotal < 1) { return 0 }
  754. return Math.ceil(state.usersTotal / state.usersPageSize)
  755. })
  756. // WATCHERS
  757. watch(() => route.params.section, checkRoute)
  758. watch([() => state.usersPage, () => state.usersFilter], refreshUsers)
  759. // METHODS
  760. function close () {
  761. adminStore.$patch({ overlay: '' })
  762. }
  763. function checkRoute () {
  764. if (!route.params.section) {
  765. router.replace({ params: { section: 'overview' } })
  766. } else if (route.params.section === 'users') {
  767. refreshUsers()
  768. }
  769. }
  770. function humanizeDate (val) {
  771. if (!val) { return '---' }
  772. return DateTime.fromISO(val).toLocaleString(DateTime.DATETIME_FULL)
  773. }
  774. function getRuleModeColor (mode) {
  775. return ({
  776. DENY: 'text-negative',
  777. ALLOW: 'text-positive',
  778. FORCEALLOW: 'text-blue'
  779. })[mode]
  780. }
  781. function getRuleModeClass (mode) {
  782. return 'is-' + mode.toLowerCase()
  783. }
  784. function getRuleModeIcon (mode) {
  785. return ({
  786. DENY: 'las la-ban',
  787. ALLOW: 'las la-check',
  788. FORCEALLOW: 'las la-check-double'
  789. })[mode] || 'las la-frog'
  790. }
  791. function getNextRuleMode (mode) {
  792. return ({
  793. DENY: 'FORCEALLOW',
  794. ALLOW: 'DENY',
  795. FORCEALLOW: 'ALLOW'
  796. })[mode] || 'ALLOW'
  797. }
  798. function getRuleModeName (mode) {
  799. switch (mode) {
  800. case 'ALLOW': return t('admin.groups.ruleAllow')
  801. case 'DENY': return t('admin.groups.ruleDeny')
  802. case 'FORCEALLOW': return t('admin.groups.ruleForceAllow')
  803. default: return '???'
  804. }
  805. }
  806. function refresh () {
  807. fetchGroup()
  808. }
  809. async function fetchGroup () {
  810. state.isLoading = true
  811. try {
  812. const resp = await APOLLO_CLIENT.query({
  813. query: gql`
  814. query adminFetchGroup (
  815. $id: UUID!
  816. ) {
  817. groupById(
  818. id: $id
  819. ) {
  820. id
  821. name
  822. redirectOnLogin
  823. redirectOnFirstLogin
  824. redirectOnLogout
  825. isSystem
  826. permissions
  827. rules {
  828. id
  829. name
  830. path
  831. roles
  832. match
  833. mode
  834. locales
  835. sites
  836. }
  837. userCount
  838. createdAt
  839. updatedAt
  840. }
  841. }
  842. `,
  843. variables: {
  844. id: adminStore.overlayOpts.id
  845. },
  846. fetchPolicy: 'network-only'
  847. })
  848. if (resp?.data?.groupById) {
  849. state.group = cloneDeep(resp.data.groupById)
  850. state.usersTotal = state.group.userCount ?? 0
  851. } else {
  852. throw new Error('An unexpected error occured while fetching group details.')
  853. }
  854. } catch (err) {
  855. $q.notify({
  856. type: 'negative',
  857. message: err.message
  858. })
  859. }
  860. state.isLoading = false
  861. }
  862. function newRule () {
  863. state.group.rules.push({
  864. id: uuid(),
  865. name: t('admin.groups.ruleUntitled'),
  866. mode: 'ALLOW',
  867. match: 'START',
  868. roles: [],
  869. path: '',
  870. locales: [],
  871. sites: []
  872. })
  873. }
  874. function deleteRule (id) {
  875. state.group.rules = state.group.rules.filter(r => r.id !== id)
  876. }
  877. function exportRules () {
  878. if (state.group.rules.length < 1) {
  879. return $q.notify({
  880. type: 'negative',
  881. message: t('admin.groups.exportRulesNoneError')
  882. })
  883. }
  884. exportFile('rules.json', JSON.stringify(state.group.rules, null, 2), { mimeType: 'application/json;charset=UTF-8' })
  885. }
  886. async function importRules () {
  887. try {
  888. const blob = await fileOpen({
  889. mimeTypes: ['application/json'],
  890. extensions: ['.json'],
  891. startIn: 'downloads',
  892. excludeAcceptAllOption: true
  893. })
  894. const rulesRaw = await blob.text()
  895. const rules = JSON.parse(rulesRaw)
  896. if (!Array.isArray(rules) || rules.length < 1) {
  897. throw new Error('Invalid Rules Format')
  898. }
  899. $q.dialog({
  900. title: t('admin.groups.importModeTitle'),
  901. message: t('admin.groups.importModeText'),
  902. options: {
  903. model: 'replace',
  904. type: 'radio',
  905. items: [
  906. { label: t('admin.groups.importModeReplace'), value: 'replace' },
  907. { label: t('admin.groups.importModeAdd'), value: 'add' }
  908. ]
  909. },
  910. persistent: true
  911. }).onOk(choice => {
  912. if (choice === 'replace') {
  913. state.group.rules = []
  914. }
  915. state.group.rules = [
  916. ...state.group.rules,
  917. ...rules.map(r => ({
  918. id: uuid(),
  919. name: r.name || t('admin.groups.ruleUntitled'),
  920. mode: ['ALLOW', 'DENY', 'FORCEALLOW'].includes(r.mode) ? r.mode : 'DENY',
  921. match: ['START', 'END', 'REGEX', 'TAG', 'TAGALL', 'EXACT'].includes(r.match) ? r.match : 'START',
  922. roles: r.roles || [],
  923. path: r.path || '',
  924. locales: r.locales.filter(l => some(adminStore.locales, ['code', l])),
  925. sites: r.sites.filter(s => some(adminStore.sites, ['id', s]))
  926. }))
  927. ]
  928. $q.notify({
  929. type: 'positive',
  930. message: t('admin.groups.importSuccess')
  931. })
  932. })
  933. } catch (err) {
  934. $q.notify({
  935. type: 'negative',
  936. message: t('admin.groups.importFailed') + ` [${err.message}]`
  937. })
  938. }
  939. }
  940. async function refreshUsers () {
  941. state.isLoadingUsers = true
  942. try {
  943. const resp = await APOLLO_CLIENT.query({
  944. query: gql`
  945. query adminFetchGroupUsers (
  946. $filter: String
  947. $page: Int
  948. $pageSize: Int
  949. $groupId: UUID!
  950. ) {
  951. groupById (
  952. id: $groupId
  953. ) {
  954. id
  955. userCount
  956. users (
  957. filter: $filter
  958. page: $page
  959. pageSize: $pageSize
  960. ) {
  961. id
  962. name
  963. email
  964. isSystem
  965. isActive
  966. createdAt
  967. lastLoginAt
  968. }
  969. }
  970. }
  971. `,
  972. variables: {
  973. filter: state.usersFilter,
  974. page: state.usersPage,
  975. pageSize: state.usersPageSize,
  976. groupId: adminStore.overlayOpts.id
  977. },
  978. fetchPolicy: 'network-only'
  979. })
  980. if (resp?.data?.groupById?.users) {
  981. state.usersTotal = resp.data.groupById.userCount ?? 0
  982. state.users = cloneDeep(resp.data.groupById.users)
  983. } else {
  984. throw new Error('An unexpected error occured while fetching group users.')
  985. }
  986. } catch (err) {
  987. $q.notify({
  988. type: 'negative',
  989. message: err.message
  990. })
  991. }
  992. state.isLoadingUsers = false
  993. }
  994. function assignUser () {
  995. }
  996. function unassignUser () {
  997. }
  998. // MOUNTED
  999. onMounted(() => {
  1000. checkRoute()
  1001. fetchGroup()
  1002. })
  1003. </script>
  1004. <style lang="scss">
  1005. .admin-groups-rule {
  1006. position: relative;
  1007. padding: 10px 0 24px 40px;
  1008. &-icon {
  1009. position: absolute;
  1010. top: 0;
  1011. left: 0;
  1012. bottom: 0;
  1013. width: 31px;
  1014. &::before {
  1015. position: absolute;
  1016. content: "";
  1017. border-radius: 100%;
  1018. width: 31px;
  1019. height: 31px;
  1020. background-color: currentColor;
  1021. top: 4px;
  1022. }
  1023. &::after {
  1024. position: absolute;
  1025. content: "";
  1026. width: 3px;
  1027. top: 41px;
  1028. bottom: 0;
  1029. left: 14px;
  1030. opacity: .4;
  1031. background-color: currentColor;
  1032. display: block;
  1033. }
  1034. .q-icon {
  1035. position: absolute;
  1036. top: 0;
  1037. left: 0;
  1038. right: 0;
  1039. font-size: 16px;
  1040. height: 38px;
  1041. line-height: 38px;
  1042. width: 100%;
  1043. align-items: center;
  1044. justify-content: center;
  1045. display: flex;
  1046. }
  1047. }
  1048. &-name {
  1049. line-height: 12px;
  1050. display: flex;
  1051. flex-wrap: nowrap;
  1052. padding-top: 4px;
  1053. &-text {
  1054. flex: 0 0;
  1055. white-space: nowrap;
  1056. }
  1057. input {
  1058. font-weight: 700;
  1059. color: $grey-6;
  1060. letter-spacing: 1px;
  1061. font-size: 12px;
  1062. line-height: 12px;
  1063. border: none;
  1064. padding: 0 0 0 5px;
  1065. outline: none;
  1066. flex: 1;
  1067. background-color: transparent;
  1068. &::placeholder {
  1069. color: $grey-5;
  1070. }
  1071. @at-root .body--dark & {
  1072. color: rgba(255,255,255,.7);
  1073. &::placeholder {
  1074. color: rgba(255,255,255,.4);
  1075. }
  1076. }
  1077. }
  1078. }
  1079. &-card {
  1080. background-color: $grey-2 !important;
  1081. @at-root .body--dark & {
  1082. background-color: $dark-6 !important;
  1083. }
  1084. &-permissions {
  1085. background-color: rgba($positive, .1);
  1086. border-bottom: 1px solid rgba($positive, .3);
  1087. display: flex;
  1088. align-items: center;
  1089. .q-select {
  1090. flex-basis: 100%;
  1091. }
  1092. &.is-allow {
  1093. background-color: rgba($positive, .1);
  1094. border-bottom: 1px solid rgba($positive, .3);
  1095. }
  1096. &.is-deny {
  1097. background-color: rgba($negative, .1);
  1098. border-bottom: 1px solid rgba($negative, .3);
  1099. }
  1100. &.is-forceallow {
  1101. background-color: rgba($blue, .1);
  1102. border-bottom: 1px solid rgba($blue, .3);
  1103. }
  1104. }
  1105. &-filters {
  1106. background-color: $grey-3;
  1107. flex-basis: 300px;
  1108. .text-caption:first-child {
  1109. color: $grey-7;
  1110. }
  1111. @at-root .body--dark & {
  1112. background-color: $dark-5;
  1113. }
  1114. }
  1115. &-pattern {
  1116. flex-grow: 1;
  1117. }
  1118. }
  1119. }
  1120. </style>