feat(admin): migrate groups dialogs to vue 3 composable

parent 7e344fc6
...@@ -131,9 +131,9 @@ groups: ...@@ -131,9 +131,9 @@ groups:
- 'read:assets' - 'read:assets'
- 'read:comments' - 'read:comments'
- 'write:comments' - 'write:comments'
defaultPageRules: defaultRules:
- id: default - name: Default Rule
deny: false mode: ALLOW
match: START match: START
roles: roles:
- 'read:pages' - 'read:pages'
...@@ -142,6 +142,7 @@ groups: ...@@ -142,6 +142,7 @@ groups:
- 'write:comments' - 'write:comments'
path: '' path: ''
locales: [] locales: []
sites: []
reservedPaths: reservedPaths:
- login - login
- logout - logout
......
const graphHelper = require('../../helpers/graph') const graphHelper = require('../../helpers/graph')
const safeRegex = require('safe-regex') const safeRegex = require('safe-regex')
const _ = require('lodash') const _ = require('lodash')
const gql = require('graphql') const { v4: uuid } = require('uuid')
/* global WIKI */ /* global WIKI */
...@@ -30,13 +30,13 @@ module.exports = { ...@@ -30,13 +30,13 @@ module.exports = {
async assignUserToGroup (obj, args, { req }) { async assignUserToGroup (obj, args, { req }) {
// Check for guest user // Check for guest user
if (args.userId === 2) { if (args.userId === 2) {
throw new gql.GraphQLError('Cannot assign the Guest user to a group.') throw new Error('Cannot assign the Guest user to a group.')
} }
// Check for valid group // Check for valid group
const grp = await WIKI.models.groups.query().findById(args.groupId) const grp = await WIKI.models.groups.query().findById(args.groupId)
if (!grp) { if (!grp) {
throw new gql.GraphQLError('Invalid Group ID') throw new Error('Invalid Group ID')
} }
// Check assigned permissions for write:groups // Check assigned permissions for write:groups
...@@ -47,13 +47,13 @@ module.exports = { ...@@ -47,13 +47,13 @@ module.exports = {
return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType) return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType)
}) })
) { ) {
throw new gql.GraphQLError('You are not authorized to assign a user to this elevated group.') throw new Error('You are not authorized to assign a user to this elevated group.')
} }
// Check for valid user // Check for valid user
const usr = await WIKI.models.users.query().findById(args.userId) const usr = await WIKI.models.users.query().findById(args.userId)
if (!usr) { if (!usr) {
throw new gql.GraphQLError('Invalid User ID') throw new Error('Invalid User ID')
} }
// Check for existing relation // Check for existing relation
...@@ -62,7 +62,7 @@ module.exports = { ...@@ -62,7 +62,7 @@ module.exports = {
groupId: args.groupId groupId: args.groupId
}).first() }).first()
if (relExist) { if (relExist) {
throw new gql.GraphQLError('User is already assigned to group.') throw new Error('User is already assigned to group.')
} }
// Assign user to group // Assign user to group
...@@ -73,7 +73,7 @@ module.exports = { ...@@ -73,7 +73,7 @@ module.exports = {
WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' }) WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' })
return { return {
responseResult: graphHelper.generateSuccess('User has been assigned to group.') operation: graphHelper.generateSuccess('User has been assigned to group.')
} }
}, },
/** /**
...@@ -83,13 +83,16 @@ module.exports = { ...@@ -83,13 +83,16 @@ module.exports = {
const group = await WIKI.models.groups.query().insertAndFetch({ const group = await WIKI.models.groups.query().insertAndFetch({
name: args.name, name: args.name,
permissions: JSON.stringify(WIKI.data.groups.defaultPermissions), permissions: JSON.stringify(WIKI.data.groups.defaultPermissions),
pageRules: JSON.stringify(WIKI.data.groups.defaultPageRules), rules: JSON.stringify(WIKI.data.groups.defaultRules.map(r => ({
id: uuid(),
...r
}))),
isSystem: false isSystem: false
}) })
await WIKI.auth.reloadGroups() await WIKI.auth.reloadGroups()
WIKI.events.outbound.emit('reloadGroups') WIKI.events.outbound.emit('reloadGroups')
return { return {
responseResult: graphHelper.generateSuccess('Group created successfully.'), operation: graphHelper.generateSuccess('Group created successfully.'),
group group
} }
}, },
...@@ -98,7 +101,7 @@ module.exports = { ...@@ -98,7 +101,7 @@ module.exports = {
*/ */
async deleteGroup (obj, args) { async deleteGroup (obj, args) {
if (args.id === 1 || args.id === 2) { if (args.id === 1 || args.id === 2) {
throw new gql.GraphQLError('Cannot delete this group.') throw new Error('Cannot delete this group.')
} }
await WIKI.models.groups.query().deleteById(args.id) await WIKI.models.groups.query().deleteById(args.id)
...@@ -110,7 +113,7 @@ module.exports = { ...@@ -110,7 +113,7 @@ module.exports = {
WIKI.events.outbound.emit('reloadGroups') WIKI.events.outbound.emit('reloadGroups')
return { return {
responseResult: graphHelper.generateSuccess('Group has been deleted.') operation: graphHelper.generateSuccess('Group has been deleted.')
} }
}, },
/** /**
...@@ -118,18 +121,18 @@ module.exports = { ...@@ -118,18 +121,18 @@ module.exports = {
*/ */
async unassignUserFromGroup (obj, args) { async unassignUserFromGroup (obj, args) {
if (args.userId === 2) { if (args.userId === 2) {
throw new gql.GraphQLError('Cannot unassign Guest user') throw new Error('Cannot unassign Guest user')
} }
if (args.userId === 1 && args.groupId === 1) { if (args.userId === 1 && args.groupId === 1) {
throw new gql.GraphQLError('Cannot unassign Administrator user from Administrators group.') throw new Error('Cannot unassign Administrator user from Administrators group.')
} }
const grp = await WIKI.models.groups.query().findById(args.groupId) const grp = await WIKI.models.groups.query().findById(args.groupId)
if (!grp) { if (!grp) {
throw new gql.GraphQLError('Invalid Group ID') throw new Error('Invalid Group ID')
} }
const usr = await WIKI.models.users.query().findById(args.userId) const usr = await WIKI.models.users.query().findById(args.userId)
if (!usr) { if (!usr) {
throw new gql.GraphQLError('Invalid User ID') throw new Error('Invalid User ID')
} }
await grp.$relatedQuery('users').unrelate().where('userId', usr.id) await grp.$relatedQuery('users').unrelate().where('userId', usr.id)
...@@ -137,7 +140,7 @@ module.exports = { ...@@ -137,7 +140,7 @@ module.exports = {
WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' }) WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' })
return { return {
responseResult: graphHelper.generateSuccess('User has been unassigned from group.') operation: graphHelper.generateSuccess('User has been unassigned from group.')
} }
}, },
/** /**
...@@ -148,7 +151,7 @@ module.exports = { ...@@ -148,7 +151,7 @@ module.exports = {
if (_.some(args.pageRules, pr => { if (_.some(args.pageRules, pr => {
return pr.match === 'REGEX' && !safeRegex(pr.path) return pr.match === 'REGEX' && !safeRegex(pr.path)
})) { })) {
throw new gql.GraphQLError('Some Page Rules contains unsafe or exponential time regex.') throw new Error('Some Page Rules contains unsafe or exponential time regex.')
} }
// Set default redirect on login value // Set default redirect on login value
...@@ -164,7 +167,7 @@ module.exports = { ...@@ -164,7 +167,7 @@ module.exports = {
return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType) return ['users', 'groups', 'navigation', 'theme', 'api', 'system'].includes(resType)
}) })
) { ) {
throw new gql.GraphQLError('You are not authorized to manage this group or assign these permissions.') throw new Error('You are not authorized to manage this group or assign these permissions.')
} }
// Check assigned permissions for manage:groups // Check assigned permissions for manage:groups
...@@ -172,7 +175,7 @@ module.exports = { ...@@ -172,7 +175,7 @@ module.exports = {
WIKI.auth.checkExclusiveAccess(req.user, ['manage:groups'], ['manage:system']) && WIKI.auth.checkExclusiveAccess(req.user, ['manage:groups'], ['manage:system']) &&
args.permissions.some(p => _.last(p.split(':')) === 'system') args.permissions.some(p => _.last(p.split(':')) === 'system')
) { ) {
throw new gql.GraphQLError('You are not authorized to manage this group or assign the manage:system permissions.') throw new Error('You are not authorized to manage this group or assign the manage:system permissions.')
} }
// Update group // Update group
...@@ -192,7 +195,7 @@ module.exports = { ...@@ -192,7 +195,7 @@ module.exports = {
WIKI.events.outbound.emit('reloadGroups') WIKI.events.outbound.emit('reloadGroups')
return { return {
responseResult: graphHelper.generateSuccess('Group has been updated.') operation: graphHelper.generateSuccess('Group has been updated.')
} }
} }
}, },
......
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"jsx": "preserve",
"paths": { "paths": {
"src/*": [ "src/*": [
"src/*" "src/*"
...@@ -36,4 +37,4 @@ ...@@ -36,4 +37,4 @@
".quasar", ".quasar",
"node_modules" "node_modules"
] ]
} }
\ No newline at end of file
<template lang="pug"> <template lang="pug">
q-dialog(ref='dialog', @hide='onDialogHide') q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 450px;') q-card(style='min-width: 450px;')
q-card-section.card-header q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-plus-plus.svg', left, size='sm') q-icon(name='img:/_assets/icons/fluent-plus-plus.svg', left, size='sm')
span {{$t(`admin.groups.create`)}} span {{t(`admin.groups.create`)}}
q-form.q-py-sm(ref='createGroupForm', @submit='create') q-form.q-py-sm(ref='createGroupForm', @submit='create')
q-item q-item
blueprint-icon(icon='team') blueprint-icon(icon='team')
q-item-section q-item-section
q-input( q-input(
outlined outlined
v-model='groupName' v-model='state.groupName'
dense dense
:rules=`[ :rules='groupNameValidation'
val => val.length > 0 || $t('admin.groups.nameMissing'),
val => /^[^<>"]+$/.test(val) || $t('admin.groups.nameInvalidChars')
]`
hide-bottom-space hide-bottom-space
:label='$t(`common.field.name`)' :label='t(`common.field.name`)'
:aria-label='$t(`common.field.name`)' :aria-label='t(`common.field.name`)'
lazy-rules='ondemand' lazy-rules='ondemand'
autofocus autofocus
) )
...@@ -26,86 +23,103 @@ q-dialog(ref='dialog', @hide='onDialogHide') ...@@ -26,86 +23,103 @@ q-dialog(ref='dialog', @hide='onDialogHide')
q-space q-space
q-btn.acrylic-btn( q-btn.acrylic-btn(
flat flat
:label='$t(`common.actions.cancel`)' :label='t(`common.actions.cancel`)'
color='grey' color='grey'
padding='xs md' padding='xs md'
@click='hide' @click='onDialogCancel'
) )
q-btn( q-btn(
unelevated unelevated
:label='$t(`common.actions.create`)' :label='t(`common.actions.create`)'
color='primary' color='primary'
padding='xs md' padding='xs md'
@click='create' @click='create'
:loading='isLoading' :loading='state.isLoading'
) )
</template> </template>
<script> <script setup>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
import { reactive, ref } from 'vue'
export default { // EMITS
emits: ['ok', 'hide'],
data () { defineEmits([
return { ...useDialogPluginComponent.emits
groupName: '', ])
isLoading: false
// QUASAR
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
const $q = useQuasar()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
groupName: '',
isLoading: false
})
// REFS
const createGroupForm = ref(null)
// VALIDATION RULES
const groupNameValidation = [
val => val.length > 0 || t('admin.groups.nameMissing'),
val => /^[^<>"]+$/.test(val) || t('admin.groups.nameInvalidChars')
]
// METHODS
async function create () {
state.isLoading = true
try {
const isFormValid = await createGroupForm.value.validate(true)
if (!isFormValid) {
throw new Error(t('admin.groups.createInvalidData'))
} }
}, const resp = await APOLLO_CLIENT.mutate({
methods: { mutation: gql`
show () { mutation createGroup (
this.$refs.dialog.show() $name: String!
}, ) {
hide () { createGroup(
this.$refs.dialog.hide() name: $name
}, ) {
onDialogHide () { operation {
this.$emit('hide') succeeded
}, message
async create () {
this.isLoading = true
try {
const isFormValid = await this.$refs.createGroupForm.validate(true)
if (!isFormValid) {
throw new Error(this.$t('admin.groups.createInvalidData'))
}
const resp = await this.$apollo.mutate({
mutation: gql`
mutation createGroup (
$name: String!
) {
createGroup(
name: $name
) {
status {
succeeded
message
}
}
} }
`,
variables: {
name: this.groupName
} }
})
if (resp?.data?.createGroup?.status?.succeeded) {
this.$q.notify({
type: 'positive',
message: this.$t('admin.groups.createSuccess')
})
this.$emit('ok')
this.hide()
} else {
throw new Error(resp?.data?.createGroup?.status?.message || 'An unexpected error occured.')
} }
} catch (err) { `,
this.$q.notify({ variables: {
type: 'negative', name: state.groupName
message: err.message
})
} }
this.isLoading = false })
if (resp?.data?.createGroup?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('admin.groups.createSuccess')
})
onDialogOK()
} else {
throw new Error(resp?.data?.createGroup?.operation?.message || 'An unexpected error occured.')
} }
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
} }
state.isLoading = false
} }
</script> </script>
<template lang="pug"> <template lang="pug">
q-dialog(ref='dialog', @hide='onDialogHide') q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 350px; max-width: 450px;') q-card(style='min-width: 350px; max-width: 450px;')
q-card-section.card-header q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-delete-bin.svg', left, size='sm') q-icon(name='img:/_assets/icons/fluent-delete-bin.svg', left, size='sm')
span {{$t(`admin.groups.delete`)}} span {{t(`admin.groups.delete`)}}
q-card-section q-card-section
.text-body2 .text-body2
i18n-t(keypath='admin.groups.deleteConfirm') i18n-t(keypath='admin.groups.deleteConfirm')
template(#groupName) template(#groupName)
strong {{group.name}} strong {{props.group.name}}
.text-body2.q-mt-md .text-body2.q-mt-md
strong.text-negative {{$t(`admin.groups.deleteConfirmWarn`)}} strong.text-negative {{t(`admin.groups.deleteConfirmWarn`)}}
q-card-actions.card-actions q-card-actions.card-actions
q-space q-space
q-btn.acrylic-btn( q-btn.acrylic-btn(
flat flat
:label='$t(`common.actions.cancel`)' :label='t(`common.actions.cancel`)'
color='grey' color='grey'
padding='xs md' padding='xs md'
@click='hide' @click='onDialogCancel'
) )
q-btn( q-btn(
unelevated unelevated
:label='$t(`common.actions.delete`)' :label='t(`common.actions.delete`)'
color='negative' color='negative'
padding='xs md' padding='xs md'
@click='confirm' @click='confirm'
) )
</template> </template>
<script> <script setup>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
export default { // PROPS
props: {
group: { const props = defineProps({
type: Object, group: {
required: true type: Object,
} required: true
}, }
emits: ['ok', 'hide'], })
data () {
return { // EMITS
}
}, defineEmits([
methods: { ...useDialogPluginComponent.emits
show () { ])
this.$refs.dialog.show()
}, // QUASAR
hide () {
this.$refs.dialog.hide() const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
}, const $q = useQuasar()
onDialogHide () {
this.$emit('hide') // I18N
},
async confirm () { const { t } = useI18n()
try {
const resp = await this.$apollo.mutate({ // METHODS
mutation: gql`
mutation deleteGroup ($id: UUID!) { async function confirm () {
deleteGroup(id: $id) { try {
status { const resp = await APOLLO_CLIENT.mutate({
succeeded mutation: gql`
message mutation deleteGroup ($id: UUID!) {
} deleteGroup(id: $id) {
} operation {
succeeded
message
} }
`,
variables: {
id: this.group.id
} }
})
if (resp?.data?.deleteGroup?.status?.succeeded) {
this.$q.notify({
type: 'positive',
message: this.$t('admin.groups.deleteSuccess')
})
this.$emit('ok')
this.hide()
} else {
throw new Error(resp?.data?.deleteGroup?.status?.message || 'An unexpected error occured.')
} }
} catch (err) { `,
this.$q.notify({ variables: {
type: 'negative', id: props.group.id
message: err.message
})
} }
})
if (resp?.data?.deleteGroup?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('admin.groups.deleteSuccess')
})
onDialogOK()
} else {
throw new Error(resp?.data?.deleteGroup?.operation?.message || 'An unexpected error occured.')
} }
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
} }
} }
</script> </script>
...@@ -3,24 +3,24 @@ q-layout(view='hHh lpR fFf', container) ...@@ -3,24 +3,24 @@ q-layout(view='hHh lpR fFf', container)
q-header.card-header.q-px-md.q-py-sm q-header.card-header.q-px-md.q-py-sm
q-icon(name='img:/_assets/icons/fluent-people.svg', left, size='md') q-icon(name='img:/_assets/icons/fluent-people.svg', left, size='md')
div div
span {{$t(`admin.groups.edit`)}} span {{t(`admin.groups.edit`)}}
.text-caption {{group.name}} .text-caption {{state.group.name}}
q-space q-space
q-btn-group(push) q-btn-group(push)
q-btn( q-btn(
push push
color='grey-6' color='grey-6'
text-color='white' text-color='white'
:aria-label='$t(`common.actions.refresh`)' :aria-label='t(`common.actions.refresh`)'
icon='las la-redo-alt' icon='las la-redo-alt'
@click='refresh' @click='refresh'
) )
q-tooltip(anchor='center left', self='center right') {{$t(`common.actions.refresh`)}} q-tooltip(anchor='center left', self='center right') {{t(`common.actions.refresh`)}}
q-btn( q-btn(
push push
color='white' color='white'
text-color='grey-7' text-color='grey-7'
:label='$t(`common.actions.close`)' :label='t(`common.actions.close`)'
icon='las la-times' icon='las la-times'
@click='close' @click='close'
) )
...@@ -28,11 +28,11 @@ q-layout(view='hHh lpR fFf', container) ...@@ -28,11 +28,11 @@ q-layout(view='hHh lpR fFf', container)
push push
color='positive' color='positive'
text-color='white' text-color='white'
:label='$t(`common.actions.save`)' :label='t(`common.actions.save`)'
icon='las la-check' icon='las la-check'
) )
q-drawer.bg-dark-6(:model-value='true', :width='250', dark) q-drawer.bg-dark-6(:model-value='true', :width='250', dark)
q-list(padding, v-show='!isLoading') q-list(padding, v-show='!state.isLoading')
q-item( q-item(
v-for='sc of sections' v-for='sc of sections'
:key='`section-` + sc.key' :key='`section-` + sc.key'
...@@ -45,109 +45,107 @@ q-layout(view='hHh lpR fFf', container) ...@@ -45,109 +45,107 @@ q-layout(view='hHh lpR fFf', container)
q-icon(:name='sc.icon', color='white') q-icon(:name='sc.icon', color='white')
q-item-section {{sc.text}} q-item-section {{sc.text}}
q-item-section(side, v-if='sc.usersTotal') q-item-section(side, v-if='sc.usersTotal')
q-badge(color='dark-3', :label='usersTotal') q-badge(color='dark-3', :label='state.usersTotal')
q-item-section(side, v-if='sc.rulesTotal && group.rules') q-item-section(side, v-if='sc.rulesTotal && state.group.rules')
q-badge(color='dark-3', :label='group.rules.length') q-badge(color='dark-3', :label='state.group.rules.length')
q-page-container q-page-container
q-page(v-if='isLoading') q-page(v-if='state.isLoading')
//- ----------------------------------------------------------------------- //- -----------------------------------------------------------------------
//- OVERVIEW //- OVERVIEW
//- ----------------------------------------------------------------------- //- -----------------------------------------------------------------------
q-page(v-else-if='$route.params.section === `overview`') q-page(v-else-if='route.params.section === `overview`')
.q-pa-md .q-pa-md
.row.q-col-gutter-md .row.q-col-gutter-md
.col-12.col-lg-8 .col-12.col-lg-8
q-card.shadow-1.q-pb-sm q-card.shadow-1.q-pb-sm
q-card-section q-card-section
.text-subtitle1 {{$t('admin.groups.general')}} .text-subtitle1 {{t('admin.groups.general')}}
q-item q-item
blueprint-icon(icon='team') blueprint-icon(icon='team')
q-item-section q-item-section
q-item-label {{$t(`admin.groups.name`)}} q-item-label {{t(`admin.groups.name`)}}
q-item-label(caption) {{$t(`admin.groups.nameHint`)}} q-item-label(caption) {{t(`admin.groups.nameHint`)}}
q-item-section q-item-section
q-input( q-input(
outlined outlined
v-model='group.name' v-model='state.group.name'
dense dense
:rules=`[ :rules='groupNameValidation'
val => /^[^<>"]+$/.test(val) || $t('admin.groups.nameInvalidChars')
]`
hide-bottom-space hide-bottom-space
:aria-label='$t(`admin.groups.name`)' :aria-label='t(`admin.groups.name`)'
) )
q-card.shadow-1.q-pb-sm.q-mt-md q-card.shadow-1.q-pb-sm.q-mt-md
q-card-section q-card-section
.text-subtitle1 {{$t('admin.groups.authBehaviors')}} .text-subtitle1 {{t('admin.groups.authBehaviors')}}
q-item q-item
blueprint-icon(icon='double-right') blueprint-icon(icon='double-right')
q-item-section q-item-section
q-item-label {{$t(`admin.groups.redirectOnLogin`)}} q-item-label {{t(`admin.groups.redirectOnLogin`)}}
q-item-label(caption) {{$t(`admin.groups.redirectOnLoginHint`)}} q-item-label(caption) {{t(`admin.groups.redirectOnLoginHint`)}}
q-item-section q-item-section
q-input( q-input(
outlined outlined
v-model='group.redirectOnLogin' v-model='state.group.redirectOnLogin'
dense dense
:aria-label='$t(`admin.groups.redirectOnLogin`)' :aria-label='t(`admin.groups.redirectOnLogin`)'
) )
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item q-item
blueprint-icon(icon='chevron-right') blueprint-icon(icon='chevron-right')
q-item-section q-item-section
q-item-label {{$t(`admin.groups.redirectOnFirstLogin`)}} q-item-label {{t(`admin.groups.redirectOnFirstLogin`)}}
q-item-label(caption) {{$t(`admin.groups.redirectOnFirstLoginHint`)}} q-item-label(caption) {{t(`admin.groups.redirectOnFirstLoginHint`)}}
q-item-section q-item-section
q-input( q-input(
outlined outlined
v-model='group.redirectOnFirstLogin' v-model='state.group.redirectOnFirstLogin'
dense dense
:aria-label='$t(`admin.groups.redirectOnLogin`)' :aria-label='t(`admin.groups.redirectOnLogin`)'
) )
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item q-item
blueprint-icon(icon='exit') blueprint-icon(icon='exit')
q-item-section q-item-section
q-item-label {{$t(`admin.groups.redirectOnLogout`)}} q-item-label {{t(`admin.groups.redirectOnLogout`)}}
q-item-label(caption) {{$t(`admin.groups.redirectOnLogoutHint`)}} q-item-label(caption) {{t(`admin.groups.redirectOnLogoutHint`)}}
q-item-section q-item-section
q-input( q-input(
outlined outlined
v-model='group.redirectOnLogout' v-model='state.group.redirectOnLogout'
dense dense
:aria-label='$t(`admin.groups.redirectOnLogout`)' :aria-label='t(`admin.groups.redirectOnLogout`)'
) )
.col-12.col-lg-4 .col-12.col-lg-4
q-card.shadow-1.q-pb-sm q-card.shadow-1.q-pb-sm
q-card-section q-card-section
.text-subtitle1 {{$t('admin.groups.info')}} .text-subtitle1 {{t('admin.groups.info')}}
q-item q-item
blueprint-icon(icon='team', :hue-rotate='-45') blueprint-icon(icon='team', :hue-rotate='-45')
q-item-section q-item-section
q-item-label {{$t(`common.field.id`)}} q-item-label {{t(`common.field.id`)}}
q-item-label: strong {{groupId}} q-item-label: strong {{state.group.id}}
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item q-item
blueprint-icon(icon='calendar-plus', :hue-rotate='-45') blueprint-icon(icon='calendar-plus', :hue-rotate='-45')
q-item-section q-item-section
q-item-label {{$t(`common.field.createdOn`)}} q-item-label {{t(`common.field.createdOn`)}}
q-item-label: strong {{humanizeDate(group.createdAt)}} q-item-label: strong {{humanizeDate(state.group.createdAt)}}
q-separator.q-my-sm(inset) q-separator.q-my-sm(inset)
q-item q-item
blueprint-icon(icon='summertime', :hue-rotate='-45') blueprint-icon(icon='summertime', :hue-rotate='-45')
q-item-section q-item-section
q-item-label {{$t(`common.field.lastUpdated`)}} q-item-label {{t(`common.field.lastUpdated`)}}
q-item-label: strong {{humanizeDate(group.updatedAt)}} q-item-label: strong {{humanizeDate(state.group.updatedAt)}}
//- ----------------------------------------------------------------------- //- -----------------------------------------------------------------------
//- RULES //- RULES
//- ----------------------------------------------------------------------- //- -----------------------------------------------------------------------
q-page(v-else-if='$route.params.section === `rules`') q-page(v-else-if='route.params.section === `rules`')
q-toolbar.q-pl-md( q-toolbar.q-pl-md(
:class='$q.dark.isActive ? `bg-dark-3` : `bg-white`' :class='$q.dark.isActive ? `bg-dark-3` : `bg-white`'
) )
.text-subtitle1 {{$t('admin.groups.rules')}} .text-subtitle1 {{t('admin.groups.rules')}}
q-space q-space
q-btn.acrylic-btn.q-mr-sm( q-btn.acrylic-btn.q-mr-sm(
icon='las la-question-circle' icon='las la-question-circle'
...@@ -163,14 +161,14 @@ q-layout(view='hHh lpR fFf', container) ...@@ -163,14 +161,14 @@ q-layout(view='hHh lpR fFf', container)
icon='las la-file-export' icon='las la-file-export'
@click='exportRules' @click='exportRules'
) )
q-tooltip {{$t('admin.groups.exportRules')}} q-tooltip {{t('admin.groups.exportRules')}}
q-btn.acrylic-btn.q-mr-sm( q-btn.acrylic-btn.q-mr-sm(
flat flat
color='indigo' color='indigo'
icon='las la-file-import' icon='las la-file-import'
@click='importRules' @click='importRules'
) )
q-tooltip {{$t('admin.groups.importRules')}} q-tooltip {{t('admin.groups.importRules')}}
q-btn( q-btn(
unelevated unelevated
color='primary' color='primary'
...@@ -181,14 +179,14 @@ q-layout(view='hHh lpR fFf', container) ...@@ -181,14 +179,14 @@ q-layout(view='hHh lpR fFf', container)
q-separator q-separator
.q-pa-md .q-pa-md
q-banner( q-banner(
v-if='!group.rules || group.rules.length < 1' v-if='!state.group.rules || state.group.rules.length < 1'
rounded rounded
:class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-4 text-grey-9`' :class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-4 text-grey-9`'
) {{$t('admin.groups.rulesNone')}} ) {{t('admin.groups.rulesNone')}}
q-card.shadow-1.q-pb-sm(v-else) q-card.shadow-1.q-pb-sm(v-else)
q-card-section q-card-section
.admin-groups-rule( .admin-groups-rule(
v-for='(rule, idx) of group.rules' v-for='(rule, idx) of state.group.rules'
:key='rule.id' :key='rule.id'
) )
.admin-groups-rule-icon(:class='getRuleModeColor(rule.mode)') .admin-groups-rule-icon(:class='getRuleModeColor(rule.mode)')
...@@ -213,7 +211,7 @@ q-layout(view='hHh lpR fFf', container) ...@@ -213,7 +211,7 @@ q-layout(view='hHh lpR fFf', container)
emit-value emit-value
map-options map-options
dense dense
:aria-label='$t(`admin.groups.ruleSites`)' :aria-label='t(`admin.groups.ruleSites`)'
:options='rules' :options='rules'
placeholder='Select permissions...' placeholder='Select permissions...'
option-value='permission' option-value='permission'
...@@ -261,13 +259,13 @@ q-layout(view='hHh lpR fFf', container) ...@@ -261,13 +259,13 @@ q-layout(view='hHh lpR fFf', container)
emit-value emit-value
map-options map-options
dense dense
:aria-label='$t(`admin.groups.ruleSites`)' :aria-label='t(`admin.groups.ruleSites`)'
:options='sites' :options='adminStore.sites'
option-value='id' option-value='id'
option-label='title' option-label='title'
multiple multiple
behavior='dialog' behavior='dialog'
:display-value='$tc(`admin.groups.selectedSites`, rule.sites.length, { count: rule.sites.length })' :display-value='t(`admin.groups.selectedSites`, rule.sites.length, { count: rule.sites.length })'
) )
template(v-slot:option='{ itemProps, itemEvents, opt, selected, toggleOption }') template(v-slot:option='{ itemProps, itemEvents, opt, selected, toggleOption }')
q-item(v-bind='itemProps', v-on='itemEvents') q-item(v-bind='itemProps', v-on='itemEvents')
...@@ -288,13 +286,13 @@ q-layout(view='hHh lpR fFf', container) ...@@ -288,13 +286,13 @@ q-layout(view='hHh lpR fFf', container)
emit-value emit-value
map-options map-options
dense dense
:aria-label='$t(`admin.groups.ruleLocales`)' :aria-label='t(`admin.groups.ruleLocales`)'
:options='locales' :options='adminStore.locales'
option-value='code' option-value='code'
option-label='name' option-label='name'
multiple multiple
behavior='dialog' behavior='dialog'
:display-value='$tc(`admin.groups.selectedLocales`, rule.locales.length, { count: rule.locales.length, locale: rule.locales.length === 1 ? rule.locales[0].toUpperCase() : `` })' :display-value='t(`admin.groups.selectedLocales`, rule.locales.length, { count: rule.locales.length, locale: rule.locales.length === 1 ? rule.locales[0].toUpperCase() : `` })'
) )
template(v-slot:option='{ itemProps, opt, selected, toggleOption }') template(v-slot:option='{ itemProps, opt, selected, toggleOption }')
q-item(v-bind='itemProps') q-item(v-bind='itemProps')
...@@ -317,14 +315,14 @@ q-layout(view='hHh lpR fFf', container) ...@@ -317,14 +315,14 @@ q-layout(view='hHh lpR fFf', container)
emit-value emit-value
map-options map-options
dense dense
:aria-label='$t(`admin.groups.ruleMatch`)' :aria-label='t(`admin.groups.ruleMatch`)'
:options=`[ :options=`[
{ label: $t('admin.groups.ruleMatchStart'), value: 'START' }, { label: t('admin.groups.ruleMatchStart'), value: 'START' },
{ label: $t('admin.groups.ruleMatchEnd'), value: 'END' }, { label: t('admin.groups.ruleMatchEnd'), value: 'END' },
{ label: $t('admin.groups.ruleMatchRegex'), value: 'REGEX' }, { label: t('admin.groups.ruleMatchRegex'), value: 'REGEX' },
{ label: $t('admin.groups.ruleMatchTag'), value: 'TAG' }, { label: t('admin.groups.ruleMatchTag'), value: 'TAG' },
{ label: $t('admin.groups.ruleMatchTagAll'), value: 'TAGALL' }, { label: t('admin.groups.ruleMatchTagAll'), value: 'TAGALL' },
{ label: $t('admin.groups.ruleMatchExact'), value: 'EXACT' } { label: t('admin.groups.ruleMatchExact'), value: 'EXACT' }
]` ]`
) )
q-input.q-mt-sm( q-input.q-mt-sm(
...@@ -333,19 +331,19 @@ q-layout(view='hHh lpR fFf', container) ...@@ -333,19 +331,19 @@ q-layout(view='hHh lpR fFf', container)
dense dense
:prefix='[`START`, `REGEX`, `EXACT`].includes(rule.match) ? `/` : null' :prefix='[`START`, `REGEX`, `EXACT`].includes(rule.match) ? `/` : null'
:suffix='rule.match === `REGEX` ? `/` : null' :suffix='rule.match === `REGEX` ? `/` : null'
:aria-label='$t(`admin.groups.rulePath`)' :aria-label='t(`admin.groups.rulePath`)'
) )
//- ----------------------------------------------------------------------- //- -----------------------------------------------------------------------
//- PERMISSIONS //- PERMISSIONS
//- ----------------------------------------------------------------------- //- -----------------------------------------------------------------------
q-page(v-else-if='$route.params.section === `permissions`') q-page(v-else-if='route.params.section === `permissions`')
.q-pa-md .q-pa-md
.row.q-col-gutter-md .row.q-col-gutter-md
.col-12.col-lg-6 .col-12.col-lg-6
q-card.shadow-1.q-pb-sm q-card.shadow-1.q-pb-sm
.flex.justify-between .flex.justify-between
q-card-section q-card-section
.text-subtitle1 {{$t(`admin.groups.permissions`)}} .text-subtitle1 {{t(`admin.groups.permissions`)}}
q-card-section q-card-section
q-btn.acrylic-btn( q-btn.acrylic-btn(
icon='las la-question-circle' icon='las la-question-circle'
...@@ -368,22 +366,22 @@ q-layout(view='hHh lpR fFf', container) ...@@ -368,22 +366,22 @@ q-layout(view='hHh lpR fFf', container)
q-item-label(caption) {{perm.hint}} q-item-label(caption) {{perm.hint}}
q-item-section(avatar) q-item-section(avatar)
q-toggle( q-toggle(
v-model='group.permissions' v-model='state.group.permissions'
:val='perm.permission' :val='perm.permission'
color='primary' color='primary'
checked-icon='las la-check' checked-icon='las la-check'
unchecked-icon='las la-times' unchecked-icon='las la-times'
:aria-label='$t(`admin.general.allowComments`)' :aria-label='t(`admin.general.allowComments`)'
) )
q-separator.q-my-sm(inset, v-if='idx < permissions.length - 1') q-separator.q-my-sm(inset, v-if='idx < permissions.length - 1')
//- ----------------------------------------------------------------------- //- -----------------------------------------------------------------------
//- USERS //- USERS
//- ----------------------------------------------------------------------- //- -----------------------------------------------------------------------
q-page(v-else-if='$route.params.section === `users`') q-page(v-else-if='route.params.section === `users`')
q-toolbar( q-toolbar(
:class='$q.dark.isActive ? `bg-dark-3` : `bg-white`' :class='$q.dark.isActive ? `bg-dark-3` : `bg-white`'
) )
.text-subtitle1 {{$t('admin.groups.users')}} .text-subtitle1 {{t('admin.groups.users')}}
q-space q-space
q-btn.acrylic-btn.q-mr-sm( q-btn.acrylic-btn.q-mr-sm(
icon='las la-question-circle' icon='las la-question-circle'
...@@ -395,8 +393,8 @@ q-layout(view='hHh lpR fFf', container) ...@@ -395,8 +393,8 @@ q-layout(view='hHh lpR fFf', container)
) )
q-input.denser.fill-outline.q-mr-sm( q-input.denser.fill-outline.q-mr-sm(
outlined outlined
v-model='usersFilter' v-model='state.usersFilter'
:placeholder='$t(`admin.groups.filterUsers`)' :placeholder='t(`admin.groups.filterUsers`)'
dense dense
) )
template(#prepend) template(#prepend)
...@@ -410,22 +408,27 @@ q-layout(view='hHh lpR fFf', container) ...@@ -410,22 +408,27 @@ q-layout(view='hHh lpR fFf', container)
q-btn.q-mr-xs( q-btn.q-mr-xs(
unelevated unelevated
icon='las la-user-plus' icon='las la-user-plus'
:label='$t(`admin.groups.assignUser`)' :label='t(`admin.groups.assignUser`)'
color='primary' color='primary'
@click='assignUser' @click='assignUser'
) )
q-separator q-separator
.q-pa-md .q-pa-md
q-banner(
v-if='!state.users || state.users.length < 1'
rounded
:class='$q.dark.isActive ? `bg-negative text-white` : `bg-grey-4 text-grey-9`'
) {{t('admin.groups.usersNone')}}
q-card.shadow-1 q-card.shadow-1
q-table( q-table(
:rows='users' :rows='state.users'
:columns='usersHeaders' :columns='usersHeaders'
row-key='id' row-key='id'
flat flat
hide-header hide-header
hide-bottom hide-bottom
:rows-per-page-options='[0]' :rows-per-page-options='[0]'
:loading='isLoadingUsers' :loading='state.isLoadingUsers'
) )
template(v-slot:body-cell-id='props') template(v-slot:body-cell-id='props')
q-td(:props='props') q-td(:props='props')
...@@ -467,7 +470,7 @@ q-layout(view='hHh lpR fFf', container) ...@@ -467,7 +470,7 @@ q-layout(view='hHh lpR fFf', container)
:to='`/_admin/users/` + props.row.id' :to='`/_admin/users/` + props.row.id'
icon='las la-pen' icon='las la-pen'
color='indigo' color='indigo'
:label='$t(`common.actions.edit`)' :label='t(`common.actions.edit`)'
no-caps no-caps
) )
q-btn.acrylic-btn( q-btn.acrylic-btn(
...@@ -480,7 +483,7 @@ q-layout(view='hHh lpR fFf', container) ...@@ -480,7 +483,7 @@ q-layout(view='hHh lpR fFf', container)
.flex.flex-center.q-mt-md(v-if='usersTotalPages > 1') .flex.flex-center.q-mt-md(v-if='usersTotalPages > 1')
q-pagination( q-pagination(
v-model='usersPage' v-model='state.usersPage'
:max='usersTotalPages' :max='usersTotalPages'
:max-pages='9' :max-pages='9'
boundary-numbers boundary-numbers
...@@ -488,517 +491,565 @@ q-layout(view='hHh lpR fFf', container) ...@@ -488,517 +491,565 @@ q-layout(view='hHh lpR fFf', container)
) )
</template> </template>
<script> <script setup>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { get } from 'vuex-pathify'
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
import cloneDeep from 'lodash/cloneDeep' import cloneDeep from 'lodash/cloneDeep'
import some from 'lodash/some' import some from 'lodash/some'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { exportFile } from 'quasar'
import { fileOpen } from 'browser-fs-access' import { fileOpen } from 'browser-fs-access'
export default { import { useI18n } from 'vue-i18n'
data () { import { exportFile, useQuasar } from 'quasar'
return { import { computed, onBeforeUnmount, onMounted, reactive, watch } from 'vue'
sections: [ import { useRouter, useRoute } from 'vue-router'
{ key: 'overview', text: this.$t('admin.groups.overview'), icon: 'las la-users' },
{ key: 'rules', text: this.$t('admin.groups.rules'), icon: 'las la-file-invoice', rulesTotal: true },
{ key: 'permissions', text: this.$t('admin.groups.permissions'), icon: 'las la-list-alt' },
{ key: 'users', text: this.$t('admin.groups.users'), icon: 'las la-user', usersTotal: true }
],
group: {
rules: []
},
isLoading: false,
// RULES
rules: [
{
permission: 'read:pages',
title: 'Read Pages',
hint: 'Can view and search pages.',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'write:pages',
title: 'Write Pages',
hint: 'Can create and edit pages.',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'review:pages',
title: 'Review Pages',
hint: 'Can review and approve edits submitted by users.',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'manage:pages',
title: 'Manage Pages',
hint: 'Can move existing pages to other locations the user has write access to.',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'delete:pages',
title: 'Delete Pages',
hint: 'Can delete existing pages.',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'write:styles',
title: 'Use CSS',
hint: 'Can insert CSS styles in pages.',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'write:scripts',
title: 'Use JavaScript',
hint: 'Can insert JavaScript in pages.',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'read:source',
title: 'View Pages Source',
hint: 'Can view pages source.',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'read:history',
title: 'View Page History',
hint: 'Can view previous versions of pages.',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'read:assets',
title: 'View Assets',
hint: 'Can view / use assets (such as images and files) in pages.',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'write:assets',
title: 'Upload Assets',
hint: 'Can upload new assets (such as images and files).',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'manage:assets',
title: 'Manage Assets',
hint: 'Can edit and delete existing assets (such as images and files).',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'read:comments',
title: 'Read Comments',
hint: 'Can view page comments.',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'write:comments',
title: 'Write Comments',
hint: 'Can post new comments on pages.',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'manage:comments',
title: 'Manage Comments',
hint: 'Can edit and delete existing page comments.',
warning: false,
restrictedForSystem: true,
disabled: false
}
],
// PERMISSIONS
permissions: [
{
permission: 'write:users',
hint: 'Can create or authorize new users, but not modify existing ones',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'manage:users',
hint: 'Can manage all users (but not users with administrative permissions)',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'write:groups',
hint: 'Can manage groups and assign CONTENT permissions / page rules',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'manage:groups',
hint: 'Can manage groups and assign ANY permissions (but not manage:system) / page rules',
warning: true,
restrictedForSystem: true,
disabled: false
},
{
permission: 'manage:navigation',
hint: 'Can manage the site navigation',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'manage:theme',
hint: 'Can manage and modify themes',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'manage:api',
hint: 'Can generate and revoke API keys',
warning: true,
restrictedForSystem: true,
disabled: false
},
{
permission: 'manage:system',
hint: 'Can manage and access everything. Root administrator.',
warning: true,
restrictedForSystem: true,
disabled: true
} import { useAdminStore } from 'src/stores/admin'
],
// USERS // QUASAR
users: [],
isLoadingUsers: false, const $q = useQuasar()
usersFilter: '',
usersPage: 1, // STORES
usersPageSize: 15,
usersTotal: 0, const adminStore = useAdminStore()
usersHeaders: [
{ // ROUTER
align: 'center',
field: 'id', const router = useRouter()
name: 'id', const route = useRoute()
sortable: false,
style: 'width: 20px' // I18N
},
{ const { t } = useI18n()
label: this.$t('common.field.name'),
align: 'left', // DATA
field: 'name',
name: 'name', const state = reactive({
sortable: true group: {
}, rules: []
{
label: this.$t('admin.users.email'),
align: 'left',
field: 'email',
name: 'email',
sortable: false
},
{
align: 'left',
field: 'createdAt',
name: 'date',
sortable: false
},
{
label: '',
align: 'right',
field: 'edit',
name: 'edit',
sortable: false,
style: 'width: 250px'
}
]
}
}, },
computed: { isLoading: false,
groupId: get('admin/overlayOpts@id', false), users: [],
sites: get('admin/sites', false), isLoadingUsers: false,
locales: get('admin/locales', false), usersFilter: '',
usersTotalPages () { usersPage: 1,
if (this.usersTotal < 1) { return 0 } usersPageSize: 15,
return Math.ceil(this.usersTotal / this.usersPageSize) usersTotal: 0
} })
const sections = [
{ key: 'overview', text: t('admin.groups.overview'), icon: 'las la-users' },
{ key: 'rules', text: t('admin.groups.rules'), icon: 'las la-file-invoice', rulesTotal: true },
{ key: 'permissions', text: t('admin.groups.permissions'), icon: 'las la-list-alt' },
{ key: 'users', text: t('admin.groups.users'), icon: 'las la-user', usersTotal: true }
]
const usersHeaders = [
{
align: 'center',
field: 'id',
name: 'id',
sortable: false,
style: 'width: 20px'
}, },
watch: { {
$route: 'checkRoute', label: t('common.field.name'),
usersPage () { align: 'left',
this.refreshUsers() field: 'name',
}, name: 'name',
usersFilter () { sortable: true
this.refreshUsers()
}
}, },
mounted () { {
this.checkRoute() label: t('admin.users.email'),
this.fetchGroup() align: 'left',
field: 'email',
name: 'email',
sortable: false
}, },
methods: { {
close () { align: 'left',
this.$store.set('admin/overlay', '') field: 'createdAt',
}, name: 'date',
checkRoute () { sortable: false
if (!this.$route.params.section) { },
this.$router.replace({ params: { section: 'overview' } }) {
} else if (this.$route.params.section === 'users') { label: '',
this.refreshUsers() align: 'right',
} field: 'edit',
}, name: 'edit',
humanizeDate (val) { sortable: false,
if (!val) { return '---' } style: 'width: 250px'
return DateTime.fromISO(val).toLocaleString(DateTime.DATETIME_FULL) }
}, ]
getRuleModeColor: (mode) => ({
DENY: 'text-negative', const permissions = [
ALLOW: 'text-positive', {
FORCEALLOW: 'text-blue' permission: 'write:users',
})[mode], hint: 'Can create or authorize new users, but not modify existing ones',
getRuleModeClass (mode) { warning: false,
return 'is-' + mode.toLowerCase() restrictedForSystem: true,
}, disabled: false
getRuleModeIcon: (mode) => ({ },
DENY: 'las la-ban', {
ALLOW: 'las la-check', permission: 'manage:users',
FORCEALLOW: 'las la-check-double' hint: 'Can manage all users (but not users with administrative permissions)',
})[mode] || 'las la-frog', warning: false,
getNextRuleMode: (mode) => ({ restrictedForSystem: true,
DENY: 'FORCEALLOW', disabled: false
ALLOW: 'DENY', },
FORCEALLOW: 'ALLOW' {
})[mode] || 'ALLOW', permission: 'write:groups',
getRuleModeName (mode) { hint: 'Can manage groups and assign CONTENT permissions / page rules',
switch (mode) { warning: false,
case 'ALLOW': return this.$t('admin.groups.ruleAllow') restrictedForSystem: true,
case 'DENY': return this.$t('admin.groups.ruleDeny') disabled: false
case 'FORCEALLOW': return this.$t('admin.groups.ruleForceAllow') },
default: return '???' {
} permission: 'manage:groups',
}, hint: 'Can manage groups and assign ANY permissions (but not manage:system) / page rules',
refresh () { warning: true,
this.fetchGroup() restrictedForSystem: true,
}, disabled: false
async fetchGroup () { },
this.isLoading = true {
try { permission: 'manage:navigation',
const resp = await this.$apollo.query({ hint: 'Can manage the site navigation',
query: gql` warning: false,
query adminFetchGroup ( restrictedForSystem: true,
$id: UUID! disabled: false
) { },
groupById( {
id: $id permission: 'manage:theme',
) { hint: 'Can manage and modify themes',
id warning: false,
name restrictedForSystem: true,
redirectOnLogin disabled: false
redirectOnFirstLogin },
redirectOnLogout {
isSystem permission: 'manage:api',
permissions hint: 'Can generate and revoke API keys',
rules { warning: true,
id restrictedForSystem: true,
name disabled: false
path },
roles {
match permission: 'manage:system',
mode hint: 'Can manage and access everything. Root administrator.',
locales warning: true,
sites restrictedForSystem: true,
} disabled: true
userCount
createdAt }
updatedAt ]
}
const rules = [
{
permission: 'read:pages',
title: 'Read Pages',
hint: 'Can view and search pages.',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'write:pages',
title: 'Write Pages',
hint: 'Can create and edit pages.',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'review:pages',
title: 'Review Pages',
hint: 'Can review and approve edits submitted by users.',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'manage:pages',
title: 'Manage Pages',
hint: 'Can move existing pages to other locations the user has write access to.',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'delete:pages',
title: 'Delete Pages',
hint: 'Can delete existing pages.',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'write:styles',
title: 'Use CSS',
hint: 'Can insert CSS styles in pages.',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'write:scripts',
title: 'Use JavaScript',
hint: 'Can insert JavaScript in pages.',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'read:source',
title: 'View Pages Source',
hint: 'Can view pages source.',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'read:history',
title: 'View Page History',
hint: 'Can view previous versions of pages.',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'read:assets',
title: 'View Assets',
hint: 'Can view / use assets (such as images and files) in pages.',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'write:assets',
title: 'Upload Assets',
hint: 'Can upload new assets (such as images and files).',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'manage:assets',
title: 'Manage Assets',
hint: 'Can edit and delete existing assets (such as images and files).',
warning: false,
restrictedForSystem: true,
disabled: false
},
{
permission: 'read:comments',
title: 'Read Comments',
hint: 'Can view page comments.',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'write:comments',
title: 'Write Comments',
hint: 'Can post new comments on pages.',
warning: false,
restrictedForSystem: false,
disabled: false
},
{
permission: 'manage:comments',
title: 'Manage Comments',
hint: 'Can edit and delete existing page comments.',
warning: false,
restrictedForSystem: true,
disabled: false
}
]
// VALIDATION RULES
const groupNameValidation = [
val => /^[^<>"]+$/.test(val) || t('admin.groups.nameInvalidChars')
]
// COMPUTED
const usersTotalPages = computed(() => {
if (state.usersTotal < 1) { return 0 }
return Math.ceil(state.usersTotal / state.usersPageSize)
})
// WATCHERS
watch(() => route.params.section, checkRoute)
watch([() => state.usersPage, () => state.usersFilter], refreshUsers)
// METHODS
function close () {
adminStore.$patch({ overlay: '' })
}
function checkRoute () {
if (!route.params.section) {
router.replace({ params: { section: 'overview' } })
} else if (route.params.section === 'users') {
refreshUsers()
}
}
function humanizeDate (val) {
if (!val) { return '---' }
return DateTime.fromISO(val).toLocaleString(DateTime.DATETIME_FULL)
}
function getRuleModeColor (mode) {
return ({
DENY: 'text-negative',
ALLOW: 'text-positive',
FORCEALLOW: 'text-blue'
})[mode]
}
function getRuleModeClass (mode) {
return 'is-' + mode.toLowerCase()
}
function getRuleModeIcon (mode) {
return ({
DENY: 'las la-ban',
ALLOW: 'las la-check',
FORCEALLOW: 'las la-check-double'
})[mode] || 'las la-frog'
}
function getNextRuleMode (mode) {
return ({
DENY: 'FORCEALLOW',
ALLOW: 'DENY',
FORCEALLOW: 'ALLOW'
})[mode] || 'ALLOW'
}
function getRuleModeName (mode) {
switch (mode) {
case 'ALLOW': return t('admin.groups.ruleAllow')
case 'DENY': return t('admin.groups.ruleDeny')
case 'FORCEALLOW': return t('admin.groups.ruleForceAllow')
default: return '???'
}
}
function refresh () {
fetchGroup()
}
async function fetchGroup () {
state.isLoading = true
try {
const resp = await APOLLO_CLIENT.query({
query: gql`
query adminFetchGroup (
$id: UUID!
) {
groupById(
id: $id
) {
id
name
redirectOnLogin
redirectOnFirstLogin
redirectOnLogout
isSystem
permissions
rules {
id
name
path
roles
match
mode
locales
sites
} }
`, userCount
variables: { createdAt
id: this.groupId updatedAt
}, }
fetchPolicy: 'network-only'
})
if (resp?.data?.groupById) {
this.group = cloneDeep(resp.data.groupById)
this.usersTotal = this.group.userCount ?? 0
} else {
throw new Error('An unexpected error occured while fetching group details.')
} }
} catch (err) { `,
this.$q.notify({ variables: {
type: 'negative', id: adminStore.overlayOpts.id
message: err.message },
}) fetchPolicy: 'network-only'
})
if (resp?.data?.groupById) {
state.group = cloneDeep(resp.data.groupById)
state.usersTotal = state.group.userCount ?? 0
} else {
throw new Error('An unexpected error occured while fetching group details.')
}
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
}
state.isLoading = false
}
function newRule () {
state.group.rules.push({
id: uuid(),
name: t('admin.groups.ruleUntitled'),
mode: 'ALLOW',
match: 'START',
roles: [],
path: '',
locales: [],
sites: []
})
}
function deleteRule (id) {
state.group.rules = state.group.rules.filter(r => r.id !== id)
}
function exportRules () {
if (state.group.rules.length < 1) {
return $q.notify({
type: 'negative',
message: t('admin.groups.exportRulesNoneError')
})
}
exportFile('rules.json', JSON.stringify(state.group.rules, null, 2), { mimeType: 'application/json;charset=UTF-8' })
}
async function importRules () {
try {
const blob = await fileOpen({
mimeTypes: ['application/json'],
extensions: ['.json'],
startIn: 'downloads',
excludeAcceptAllOption: true
})
const rulesRaw = await blob.text()
const rules = JSON.parse(rulesRaw)
if (!Array.isArray(rules) || rules.length < 1) {
throw new Error('Invalid Rules Format')
}
$q.dialog({
title: t('admin.groups.importModeTitle'),
message: t('admin.groups.importModeText'),
options: {
model: 'replace',
type: 'radio',
items: [
{ label: t('admin.groups.importModeReplace'), value: 'replace' },
{ label: t('admin.groups.importModeAdd'), value: 'add' }
]
},
persistent: true
}).onOk(choice => {
if (choice === 'replace') {
state.group.rules = []
} }
this.isLoading = false state.group.rules = [
}, ...state.group.rules,
newRule () { ...rules.map(r => ({
this.group.rules.push({ id: uuid(),
id: uuid(), name: r.name || t('admin.groups.ruleUntitled'),
name: this.$t('admin.groups.ruleUntitled'), mode: ['ALLOW', 'DENY', 'FORCEALLOW'].includes(r.mode) ? r.mode : 'DENY',
mode: 'ALLOW', match: ['START', 'END', 'REGEX', 'TAG', 'TAGALL', 'EXACT'].includes(r.match) ? r.match : 'START',
match: 'START', roles: r.roles || [],
roles: [], path: r.path || '',
path: '', locales: r.locales.filter(l => some(adminStore.locales, ['code', l])),
locales: [], sites: r.sites.filter(s => some(adminStore.sites, ['id', s]))
sites: [] }))
]
$q.notify({
type: 'positive',
message: t('admin.groups.importSuccess')
}) })
}, })
deleteRule (id) { } catch (err) {
this.group.rules = this.group.rules.filter(r => r.id !== id) $q.notify({
}, type: 'negative',
exportRules () { message: t('admin.groups.importFailed') + ` [${err.message}]`
if (this.group.rules.length < 1) { })
return this.$q.notify({ }
type: 'negative', }
message: this.$t('admin.groups.exportRulesNoneError')
}) async function refreshUsers () {
} state.isLoadingUsers = true
exportFile('rules.json', JSON.stringify(this.group.rules, null, 2), { mimeType: 'application/json;charset=UTF-8' }) try {
}, const resp = await APOLLO_CLIENT.query({
async importRules () { query: gql`
try { query adminFetchGroupUsers (
const blob = await fileOpen({ $filter: String
mimeTypes: ['application/json'], $page: Int
extensions: ['.json'], $pageSize: Int
startIn: 'downloads', $groupId: UUID!
excludeAcceptAllOption: true ) {
}) groupById (
const rulesRaw = await blob.text() id: $groupId
const rules = JSON.parse(rulesRaw) ) {
if (!Array.isArray(rules) || rules.length < 1) { id
throw new Error('Invalid Rules Format') userCount
} users (
this.$q.dialog({ filter: $filter
title: this.$t('admin.groups.importModeTitle'), page: $page
message: this.$t('admin.groups.importModeText'), pageSize: $pageSize
options: { ) {
model: 'replace', id
type: 'radio', name
items: [ email
{ label: this.$t('admin.groups.importModeReplace'), value: 'replace' }, isSystem
{ label: this.$t('admin.groups.importModeAdd'), value: 'add' } isActive
] createdAt
}, lastLoginAt
persistent: true
}).onOk(choice => {
if (choice === 'replace') {
this.group.rules = []
}
this.group.rules = [
...this.group.rules,
...rules.map(r => ({
id: uuid(),
name: r.name || this.$t('admin.groups.ruleUntitled'),
mode: ['ALLOW', 'DENY', 'FORCEALLOW'].includes(r.mode) ? r.mode : 'DENY',
match: ['START', 'END', 'REGEX', 'TAG', 'TAGALL', 'EXACT'].includes(r.match) ? r.match : 'START',
roles: r.roles || [],
path: r.path || '',
locales: r.locales.filter(l => some(this.locales, ['code', l])),
sites: r.sites.filter(s => some(this.sites, ['id', s]))
}))
]
this.$q.notify({
type: 'positive',
message: this.$t('admin.groups.importSuccess')
})
})
} catch (err) {
this.$q.notify({
type: 'negative',
message: this.$t('admin.groups.importFailed') + ` [${err.message}]`
})
}
},
async refreshUsers () {
this.isLoadingUsers = true
try {
const resp = await this.$apollo.query({
query: gql`
query adminFetchGroupUsers (
$filter: String
$page: Int
$pageSize: Int
$groupId: UUID!
) {
groupById (
id: $groupId
) {
id
userCount
users (
filter: $filter
page: $page
pageSize: $pageSize
) {
id
name
email
isSystem
isActive
createdAt
lastLoginAt
}
}
} }
`, }
variables: {
filter: this.usersFilter,
page: this.usersPage,
pageSize: this.usersPageSize,
groupId: this.groupId
},
fetchPolicy: 'network-only'
})
if (resp?.data?.groupById?.users) {
this.usersTotal = resp.data.groupById.userCount ?? 0
this.users = cloneDeep(resp.data.groupById.users)
} else {
throw new Error('An unexpected error occured while fetching group users.')
} }
} catch (err) { `,
this.$q.notify({ variables: {
type: 'negative', filter: state.usersFilter,
message: err.message page: state.usersPage,
}) pageSize: state.usersPageSize,
} groupId: adminStore.overlayOpts.id
this.isLoadingUsers = false },
}, fetchPolicy: 'network-only'
assignUser () { })
if (resp?.data?.groupById?.users) {
}, state.usersTotal = resp.data.groupById.userCount ?? 0
unassignUser () { state.users = cloneDeep(resp.data.groupById.users)
} else {
throw new Error('An unexpected error occured while fetching group users.')
} }
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
} }
state.isLoadingUsers = false
}
function assignUser () {
}
function unassignUser () {
} }
// MOUNTED
onMounted(() => {
checkRoute()
fetchGroup()
})
</script> </script>
<style lang="scss"> <style lang="scss">
......
...@@ -12,10 +12,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide') ...@@ -12,10 +12,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
outlined outlined
v-model='state.siteName' v-model='state.siteName'
dense dense
:rules=`[ :rules='siteNameValidation'
val => val.length > 0 || t('admin.sites.nameMissing'),
val => /^[^<>"]+$/.test(val) || t('admin.sites.nameInvalidChars')
]`
hide-bottom-space hide-bottom-space
:label='t(`common.field.name`)' :label='t(`common.field.name`)'
:aria-label='t(`common.field.name`)' :aria-label='t(`common.field.name`)'
...@@ -29,10 +26,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide') ...@@ -29,10 +26,7 @@ q-dialog(ref='dialogRef', @hide='onDialogHide')
outlined outlined
v-model='state.siteHostname' v-model='state.siteHostname'
dense dense
:rules=`[ :rules='siteHostnameValidation'
val => val.length > 0 || t('admin.sites.hostnameMissing'),
val => /^(\\*)|([a-z0-9\-.:]+)$/.test(val) || t('admin.sites.hostnameInvalidChars')
]`
:hint='t(`admin.sites.hostnameHint`)' :hint='t(`admin.sites.hostnameHint`)'
hide-bottom-space hide-bottom-space
:label='t(`admin.sites.hostname`)' :label='t(`admin.sites.hostname`)'
...@@ -97,6 +91,17 @@ const state = reactive({ ...@@ -97,6 +91,17 @@ const state = reactive({
const createSiteForm = ref(null) const createSiteForm = ref(null)
// VALIDATION RULES
const siteNameValidation = [
val => val.length > 0 || t('admin.sites.nameMissing'),
val => /^[^<>"]+$/.test(val) || t('admin.sites.nameInvalidChars')
]
const siteHostnameValidation = [
val => val.length > 0 || t('admin.sites.hostnameMissing'),
val => /^(\\*)|([a-z0-9\-.:]+)$/.test(val) || t('admin.sites.hostnameInvalidChars')
]
// METHODS // METHODS
async function create () { async function create () {
......
...@@ -81,7 +81,7 @@ async function confirm () { ...@@ -81,7 +81,7 @@ async function confirm () {
mutation: gql` mutation: gql`
mutation deleteSite ($id: UUID!) { mutation deleteSite ($id: UUID!) {
deleteSite(id: $id) { deleteSite(id: $id) {
status { operation {
succeeded succeeded
message message
} }
...@@ -92,7 +92,7 @@ async function confirm () { ...@@ -92,7 +92,7 @@ async function confirm () {
id: props.site.id id: props.site.id
} }
}) })
if (resp?.data?.deleteSite?.status?.succeeded) { if (resp?.data?.deleteSite?.operation?.succeeded) {
$q.notify({ $q.notify({
type: 'positive', type: 'positive',
message: t('admin.sites.deleteSuccess') message: t('admin.sites.deleteSuccess')
...@@ -102,7 +102,7 @@ async function confirm () { ...@@ -102,7 +102,7 @@ async function confirm () {
}) })
onDialogOK() onDialogOK()
} else { } else {
throw new Error(resp?.data?.deleteSite?.status?.message || 'An unexpected error occured.') throw new Error(resp?.data?.deleteSite?.operation?.message || 'An unexpected error occured.')
} }
} catch (err) { } catch (err) {
$q.notify({ $q.notify({
......
...@@ -1421,5 +1421,6 @@ ...@@ -1421,5 +1421,6 @@
"tags.searchWithinResultsPlaceholder": "Search within results...", "tags.searchWithinResultsPlaceholder": "Search within results...",
"tags.selectOneMoreTags": "Select one or more tags", "tags.selectOneMoreTags": "Select one or more tags",
"tags.selectOneMoreTagsHint": "Select one or more tags on the left.", "tags.selectOneMoreTagsHint": "Select one or more tags on the left.",
"admin.general.sitemapHint": "Make a sitemap.xml available to search engines with all pages accessible to guests." "admin.general.sitemapHint": "Make a sitemap.xml available to search engines with all pages accessible to guests.",
"admin.groups.usersNone": "This group doesn't have any user yet."
} }
...@@ -177,7 +177,7 @@ q-layout.admin(view='hHh Lpr lff') ...@@ -177,7 +177,7 @@ q-layout.admin(view='hHh Lpr lff')
transition-show='jump-up' transition-show='jump-up'
transition-hide='jump-down' transition-hide='jump-down'
) )
component(:is='adminStore.overlay') component(:is='overlays[adminStore.overlay]')
q-footer.admin-footer q-footer.admin-footer
q-bar.justify-center(dense) q-bar.justify-center(dense)
span(style='font-size: 11px;') Powered by #[a(href='https://js.wiki', target='_blank'): strong Wiki.js], an open source project. span(style='font-size: 11px;') Powered by #[a(href='https://js.wiki', target='_blank'): strong Wiki.js], an open source project.
...@@ -195,8 +195,10 @@ import { useSiteStore } from '../stores/site' ...@@ -195,8 +195,10 @@ import { useSiteStore } from '../stores/site'
// COMPONENTS // COMPONENTS
import AccountMenu from '../components/AccountMenu.vue' import AccountMenu from '../components/AccountMenu.vue'
const GroupEditOverlay = defineAsyncComponent(() => import('../components/GroupEditOverlay.vue')) const overlays = {
const UserEditOverlay = defineAsyncComponent(() => import('../components/UserEditOverlay.vue')) GroupEditOverlay: defineAsyncComponent(() => import('../components/GroupEditOverlay.vue')),
UserEditOverlay: defineAsyncComponent(() => import('../components/UserEditOverlay.vue'))
}
// STORES // STORES
......
...@@ -175,7 +175,7 @@ watch(() => adminStore.overlay, (newValue, oldValue) => { ...@@ -175,7 +175,7 @@ watch(() => adminStore.overlay, (newValue, oldValue) => {
} }
}) })
watch(() => route, () => { watch(() => route.params.id, () => {
checkOverlay() checkOverlay()
}) })
...@@ -213,7 +213,7 @@ async function load () { ...@@ -213,7 +213,7 @@ async function load () {
} }
function checkOverlay () { function checkOverlay () {
if (route.params && route.params.id) { if (route.params?.id) {
adminStore.$patch({ adminStore.$patch({
overlayOpts: { id: route.params.id }, overlayOpts: { id: route.params.id },
overlay: 'GroupEditOverlay' overlay: 'GroupEditOverlay'
......
...@@ -737,7 +737,7 @@ watch(() => state.targets, (newValue) => { ...@@ -737,7 +737,7 @@ watch(() => state.targets, (newValue) => {
handleSetupCallback() handleSetupCallback()
} }
}) })
watch(() => route, (to, from) => { watch(() => route.params.id, (to, from) => {
if (!to.params.id) { if (!to.params.id) {
return return
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment