diff --git a/server/app/data.yml b/server/app/data.yml index c0dc58f5..101e4e48 100644 --- a/server/app/data.yml +++ b/server/app/data.yml @@ -131,9 +131,9 @@ groups: - 'read:assets' - 'read:comments' - 'write:comments' - defaultPageRules: - - id: default - deny: false + defaultRules: + - name: Default Rule + mode: ALLOW match: START roles: - 'read:pages' @@ -142,6 +142,7 @@ groups: - 'write:comments' path: '' locales: [] + sites: [] reservedPaths: - login - logout diff --git a/server/graph/resolvers/group.js b/server/graph/resolvers/group.js index e03014e3..25c7fe6d 100644 --- a/server/graph/resolvers/group.js +++ b/server/graph/resolvers/group.js @@ -1,7 +1,7 @@ const graphHelper = require('../../helpers/graph') const safeRegex = require('safe-regex') const _ = require('lodash') -const gql = require('graphql') +const { v4: uuid } = require('uuid') /* global WIKI */ @@ -30,13 +30,13 @@ module.exports = { async assignUserToGroup (obj, args, { req }) { // Check for guest user 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 const grp = await WIKI.models.groups.query().findById(args.groupId) if (!grp) { - throw new gql.GraphQLError('Invalid Group ID') + throw new Error('Invalid Group ID') } // Check assigned permissions for write:groups @@ -47,13 +47,13 @@ module.exports = { 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 const usr = await WIKI.models.users.query().findById(args.userId) if (!usr) { - throw new gql.GraphQLError('Invalid User ID') + throw new Error('Invalid User ID') } // Check for existing relation @@ -62,7 +62,7 @@ module.exports = { groupId: args.groupId }).first() 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 @@ -73,7 +73,7 @@ module.exports = { WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' }) 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 = { const group = await WIKI.models.groups.query().insertAndFetch({ name: args.name, 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 }) await WIKI.auth.reloadGroups() WIKI.events.outbound.emit('reloadGroups') return { - responseResult: graphHelper.generateSuccess('Group created successfully.'), + operation: graphHelper.generateSuccess('Group created successfully.'), group } }, @@ -98,7 +101,7 @@ module.exports = { */ async deleteGroup (obj, args) { 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) @@ -110,7 +113,7 @@ module.exports = { WIKI.events.outbound.emit('reloadGroups') return { - responseResult: graphHelper.generateSuccess('Group has been deleted.') + operation: graphHelper.generateSuccess('Group has been deleted.') } }, /** @@ -118,18 +121,18 @@ module.exports = { */ async unassignUserFromGroup (obj, args) { 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) { - 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) 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) if (!usr) { - throw new gql.GraphQLError('Invalid User ID') + throw new Error('Invalid User ID') } await grp.$relatedQuery('users').unrelate().where('userId', usr.id) @@ -137,7 +140,7 @@ module.exports = { WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' }) 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 = { if (_.some(args.pageRules, pr => { 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 @@ -164,7 +167,7 @@ module.exports = { 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 @@ -172,7 +175,7 @@ module.exports = { WIKI.auth.checkExclusiveAccess(req.user, ['manage:groups'], ['manage: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 @@ -192,7 +195,7 @@ module.exports = { WIKI.events.outbound.emit('reloadGroups') return { - responseResult: graphHelper.generateSuccess('Group has been updated.') + operation: graphHelper.generateSuccess('Group has been updated.') } } }, diff --git a/ux/jsconfig.json b/ux/jsconfig.json index 456944a5..d4bfff02 100644 --- a/ux/jsconfig.json +++ b/ux/jsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "baseUrl": ".", + "jsx": "preserve", "paths": { "src/*": [ "src/*" @@ -36,4 +37,4 @@ ".quasar", "node_modules" ] -} \ No newline at end of file +} diff --git a/ux/src/components/GroupCreateDialog.vue b/ux/src/components/GroupCreateDialog.vue index cd11b627..727ac1bd 100644 --- a/ux/src/components/GroupCreateDialog.vue +++ b/ux/src/components/GroupCreateDialog.vue @@ -1,24 +1,21 @@ - diff --git a/ux/src/components/GroupDeleteDialog.vue b/ux/src/components/GroupDeleteDialog.vue index e4b9f773..4123ca3b 100644 --- a/ux/src/components/GroupDeleteDialog.vue +++ b/ux/src/components/GroupDeleteDialog.vue @@ -1,93 +1,96 @@ - diff --git a/ux/src/components/GroupEditOverlay.vue b/ux/src/components/GroupEditOverlay.vue index b3228b3f..6ac2a3c5 100644 --- a/ux/src/components/GroupEditOverlay.vue +++ b/ux/src/components/GroupEditOverlay.vue @@ -3,24 +3,24 @@ q-layout(view='hHh lpR fFf', container) q-header.card-header.q-px-md.q-py-sm q-icon(name='img:/_assets/icons/fluent-people.svg', left, size='md') div - span {{$t(`admin.groups.edit`)}} - .text-caption {{group.name}} + span {{t(`admin.groups.edit`)}} + .text-caption {{state.group.name}} q-space q-btn-group(push) q-btn( push color='grey-6' text-color='white' - :aria-label='$t(`common.actions.refresh`)' + :aria-label='t(`common.actions.refresh`)' icon='las la-redo-alt' @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( push color='white' text-color='grey-7' - :label='$t(`common.actions.close`)' + :label='t(`common.actions.close`)' icon='las la-times' @click='close' ) @@ -28,11 +28,11 @@ q-layout(view='hHh lpR fFf', container) push color='positive' text-color='white' - :label='$t(`common.actions.save`)' + :label='t(`common.actions.save`)' icon='las la-check' ) 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( v-for='sc of sections' :key='`section-` + sc.key' @@ -45,109 +45,107 @@ q-layout(view='hHh lpR fFf', container) q-icon(:name='sc.icon', color='white') q-item-section {{sc.text}} q-item-section(side, v-if='sc.usersTotal') - q-badge(color='dark-3', :label='usersTotal') - q-item-section(side, v-if='sc.rulesTotal && group.rules') - q-badge(color='dark-3', :label='group.rules.length') + q-badge(color='dark-3', :label='state.usersTotal') + q-item-section(side, v-if='sc.rulesTotal && state.group.rules') + q-badge(color='dark-3', :label='state.group.rules.length') q-page-container - q-page(v-if='isLoading') + q-page(v-if='state.isLoading') //- ----------------------------------------------------------------------- //- OVERVIEW //- ----------------------------------------------------------------------- - q-page(v-else-if='$route.params.section === `overview`') + q-page(v-else-if='route.params.section === `overview`') .q-pa-md .row.q-col-gutter-md .col-12.col-lg-8 q-card.shadow-1.q-pb-sm q-card-section - .text-subtitle1 {{$t('admin.groups.general')}} + .text-subtitle1 {{t('admin.groups.general')}} q-item blueprint-icon(icon='team') q-item-section - q-item-label {{$t(`admin.groups.name`)}} - q-item-label(caption) {{$t(`admin.groups.nameHint`)}} + q-item-label {{t(`admin.groups.name`)}} + q-item-label(caption) {{t(`admin.groups.nameHint`)}} q-item-section q-input( outlined - v-model='group.name' + v-model='state.group.name' dense - :rules=`[ - val => /^[^<>"]+$/.test(val) || $t('admin.groups.nameInvalidChars') - ]` + :rules='groupNameValidation' 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-section - .text-subtitle1 {{$t('admin.groups.authBehaviors')}} + .text-subtitle1 {{t('admin.groups.authBehaviors')}} q-item blueprint-icon(icon='double-right') q-item-section - q-item-label {{$t(`admin.groups.redirectOnLogin`)}} - q-item-label(caption) {{$t(`admin.groups.redirectOnLoginHint`)}} + q-item-label {{t(`admin.groups.redirectOnLogin`)}} + q-item-label(caption) {{t(`admin.groups.redirectOnLoginHint`)}} q-item-section q-input( outlined - v-model='group.redirectOnLogin' + v-model='state.group.redirectOnLogin' dense - :aria-label='$t(`admin.groups.redirectOnLogin`)' + :aria-label='t(`admin.groups.redirectOnLogin`)' ) q-separator.q-my-sm(inset) q-item blueprint-icon(icon='chevron-right') q-item-section - q-item-label {{$t(`admin.groups.redirectOnFirstLogin`)}} - q-item-label(caption) {{$t(`admin.groups.redirectOnFirstLoginHint`)}} + q-item-label {{t(`admin.groups.redirectOnFirstLogin`)}} + q-item-label(caption) {{t(`admin.groups.redirectOnFirstLoginHint`)}} q-item-section q-input( outlined - v-model='group.redirectOnFirstLogin' + v-model='state.group.redirectOnFirstLogin' dense - :aria-label='$t(`admin.groups.redirectOnLogin`)' + :aria-label='t(`admin.groups.redirectOnLogin`)' ) q-separator.q-my-sm(inset) q-item blueprint-icon(icon='exit') q-item-section - q-item-label {{$t(`admin.groups.redirectOnLogout`)}} - q-item-label(caption) {{$t(`admin.groups.redirectOnLogoutHint`)}} + q-item-label {{t(`admin.groups.redirectOnLogout`)}} + q-item-label(caption) {{t(`admin.groups.redirectOnLogoutHint`)}} q-item-section q-input( outlined - v-model='group.redirectOnLogout' + v-model='state.group.redirectOnLogout' dense - :aria-label='$t(`admin.groups.redirectOnLogout`)' + :aria-label='t(`admin.groups.redirectOnLogout`)' ) .col-12.col-lg-4 q-card.shadow-1.q-pb-sm q-card-section - .text-subtitle1 {{$t('admin.groups.info')}} + .text-subtitle1 {{t('admin.groups.info')}} q-item blueprint-icon(icon='team', :hue-rotate='-45') q-item-section - q-item-label {{$t(`common.field.id`)}} - q-item-label: strong {{groupId}} + q-item-label {{t(`common.field.id`)}} + q-item-label: strong {{state.group.id}} q-separator.q-my-sm(inset) q-item blueprint-icon(icon='calendar-plus', :hue-rotate='-45') q-item-section - q-item-label {{$t(`common.field.createdOn`)}} - q-item-label: strong {{humanizeDate(group.createdAt)}} + q-item-label {{t(`common.field.createdOn`)}} + q-item-label: strong {{humanizeDate(state.group.createdAt)}} q-separator.q-my-sm(inset) q-item blueprint-icon(icon='summertime', :hue-rotate='-45') q-item-section - q-item-label {{$t(`common.field.lastUpdated`)}} - q-item-label: strong {{humanizeDate(group.updatedAt)}} + q-item-label {{t(`common.field.lastUpdated`)}} + q-item-label: strong {{humanizeDate(state.group.updatedAt)}} //- ----------------------------------------------------------------------- //- RULES //- ----------------------------------------------------------------------- - q-page(v-else-if='$route.params.section === `rules`') + q-page(v-else-if='route.params.section === `rules`') q-toolbar.q-pl-md( :class='$q.dark.isActive ? `bg-dark-3` : `bg-white`' ) - .text-subtitle1 {{$t('admin.groups.rules')}} + .text-subtitle1 {{t('admin.groups.rules')}} q-space q-btn.acrylic-btn.q-mr-sm( icon='las la-question-circle' @@ -163,14 +161,14 @@ q-layout(view='hHh lpR fFf', container) icon='las la-file-export' @click='exportRules' ) - q-tooltip {{$t('admin.groups.exportRules')}} + q-tooltip {{t('admin.groups.exportRules')}} q-btn.acrylic-btn.q-mr-sm( flat color='indigo' icon='las la-file-import' @click='importRules' ) - q-tooltip {{$t('admin.groups.importRules')}} + q-tooltip {{t('admin.groups.importRules')}} q-btn( unelevated color='primary' @@ -181,14 +179,14 @@ q-layout(view='hHh lpR fFf', container) q-separator .q-pa-md q-banner( - v-if='!group.rules || group.rules.length < 1' + v-if='!state.group.rules || state.group.rules.length < 1' rounded :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-section .admin-groups-rule( - v-for='(rule, idx) of group.rules' + v-for='(rule, idx) of state.group.rules' :key='rule.id' ) .admin-groups-rule-icon(:class='getRuleModeColor(rule.mode)') @@ -213,7 +211,7 @@ q-layout(view='hHh lpR fFf', container) emit-value map-options dense - :aria-label='$t(`admin.groups.ruleSites`)' + :aria-label='t(`admin.groups.ruleSites`)' :options='rules' placeholder='Select permissions...' option-value='permission' @@ -261,13 +259,13 @@ q-layout(view='hHh lpR fFf', container) emit-value map-options dense - :aria-label='$t(`admin.groups.ruleSites`)' - :options='sites' + :aria-label='t(`admin.groups.ruleSites`)' + :options='adminStore.sites' option-value='id' option-label='title' multiple 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 }') q-item(v-bind='itemProps', v-on='itemEvents') @@ -288,13 +286,13 @@ q-layout(view='hHh lpR fFf', container) emit-value map-options dense - :aria-label='$t(`admin.groups.ruleLocales`)' - :options='locales' + :aria-label='t(`admin.groups.ruleLocales`)' + :options='adminStore.locales' option-value='code' option-label='name' multiple 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 }') q-item(v-bind='itemProps') @@ -317,14 +315,14 @@ q-layout(view='hHh lpR fFf', container) emit-value map-options dense - :aria-label='$t(`admin.groups.ruleMatch`)' + :aria-label='t(`admin.groups.ruleMatch`)' :options=`[ - { label: $t('admin.groups.ruleMatchStart'), value: 'START' }, - { label: $t('admin.groups.ruleMatchEnd'), value: 'END' }, - { label: $t('admin.groups.ruleMatchRegex'), value: 'REGEX' }, - { label: $t('admin.groups.ruleMatchTag'), value: 'TAG' }, - { label: $t('admin.groups.ruleMatchTagAll'), value: 'TAGALL' }, - { label: $t('admin.groups.ruleMatchExact'), value: 'EXACT' } + { label: t('admin.groups.ruleMatchStart'), value: 'START' }, + { label: t('admin.groups.ruleMatchEnd'), value: 'END' }, + { label: t('admin.groups.ruleMatchRegex'), value: 'REGEX' }, + { label: t('admin.groups.ruleMatchTag'), value: 'TAG' }, + { label: t('admin.groups.ruleMatchTagAll'), value: 'TAGALL' }, + { label: t('admin.groups.ruleMatchExact'), value: 'EXACT' } ]` ) q-input.q-mt-sm( @@ -333,19 +331,19 @@ q-layout(view='hHh lpR fFf', container) dense :prefix='[`START`, `REGEX`, `EXACT`].includes(rule.match) ? `/` : null' :suffix='rule.match === `REGEX` ? `/` : null' - :aria-label='$t(`admin.groups.rulePath`)' + :aria-label='t(`admin.groups.rulePath`)' ) //- ----------------------------------------------------------------------- //- PERMISSIONS //- ----------------------------------------------------------------------- - q-page(v-else-if='$route.params.section === `permissions`') + q-page(v-else-if='route.params.section === `permissions`') .q-pa-md .row.q-col-gutter-md .col-12.col-lg-6 q-card.shadow-1.q-pb-sm .flex.justify-between q-card-section - .text-subtitle1 {{$t(`admin.groups.permissions`)}} + .text-subtitle1 {{t(`admin.groups.permissions`)}} q-card-section q-btn.acrylic-btn( icon='las la-question-circle' @@ -368,22 +366,22 @@ q-layout(view='hHh lpR fFf', container) q-item-label(caption) {{perm.hint}} q-item-section(avatar) q-toggle( - v-model='group.permissions' + v-model='state.group.permissions' :val='perm.permission' color='primary' checked-icon='las la-check' 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') //- ----------------------------------------------------------------------- //- USERS //- ----------------------------------------------------------------------- - q-page(v-else-if='$route.params.section === `users`') + q-page(v-else-if='route.params.section === `users`') q-toolbar( :class='$q.dark.isActive ? `bg-dark-3` : `bg-white`' ) - .text-subtitle1 {{$t('admin.groups.users')}} + .text-subtitle1 {{t('admin.groups.users')}} q-space q-btn.acrylic-btn.q-mr-sm( icon='las la-question-circle' @@ -395,8 +393,8 @@ q-layout(view='hHh lpR fFf', container) ) q-input.denser.fill-outline.q-mr-sm( outlined - v-model='usersFilter' - :placeholder='$t(`admin.groups.filterUsers`)' + v-model='state.usersFilter' + :placeholder='t(`admin.groups.filterUsers`)' dense ) template(#prepend) @@ -410,22 +408,27 @@ q-layout(view='hHh lpR fFf', container) q-btn.q-mr-xs( unelevated icon='las la-user-plus' - :label='$t(`admin.groups.assignUser`)' + :label='t(`admin.groups.assignUser`)' color='primary' @click='assignUser' ) q-separator .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-table( - :rows='users' + :rows='state.users' :columns='usersHeaders' row-key='id' flat hide-header hide-bottom :rows-per-page-options='[0]' - :loading='isLoadingUsers' + :loading='state.isLoadingUsers' ) template(v-slot:body-cell-id='props') q-td(:props='props') @@ -467,7 +470,7 @@ q-layout(view='hHh lpR fFf', container) :to='`/_admin/users/` + props.row.id' icon='las la-pen' color='indigo' - :label='$t(`common.actions.edit`)' + :label='t(`common.actions.edit`)' no-caps ) q-btn.acrylic-btn( @@ -480,7 +483,7 @@ q-layout(view='hHh lpR fFf', container) .flex.flex-center.q-mt-md(v-if='usersTotalPages > 1') q-pagination( - v-model='usersPage' + v-model='state.usersPage' :max='usersTotalPages' :max-pages='9' boundary-numbers @@ -488,517 +491,565 @@ q-layout(view='hHh lpR fFf', container) ) -