From edb97b832d14658fd566852b6cfa5486c2b84369 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Sat, 29 Dec 2018 21:30:51 -0500 Subject: [PATCH] feat: admin - manage groups + permissions + page rules --- Makefile | 5 + client/components/admin.vue | 101 +++--- client/components/admin/admin-auth.vue | 2 + client/components/admin/admin-dashboard.vue | 75 +++-- .../admin/admin-groups-edit-permissions.vue | 203 ++++++++++++ .../admin/admin-groups-edit-rules.vue | 302 +++++++++++++++++ .../admin/admin-groups-edit-users.vue | 147 +++++++++ client/components/admin/admin-groups-edit.vue | 306 ++---------------- client/components/admin/admin-groups.vue | 58 ++-- client/components/admin/admin-system.vue | 19 +- .../components/admin/admin-users-create.vue | 38 +-- client/components/common/criterias-item.vue | 211 ------------ client/components/common/criterias.vue | 173 ---------- client/components/common/nav-header.vue | 234 +++++++------- client/components/common/user-search.vue | 16 +- client/components/editor.vue | 1 + .../admin/groups/groups-mutation-update.gql | 4 +- .../admin/groups/groups-query-single.gql | 7 +- .../graph/admin/system/system-query-info.gql | 1 + client/static/svg/icon-apple-logo.svg | 4 + client/static/svg/icon-delete-file.svg | 13 + client/static/svg/icon-docker-logo.svg | 4 + client/static/svg/icon-linux-logo.svg | 4 + client/static/svg/icon-windows-logo.svg | 4 + client/themes/default/components/page.vue | 26 +- dev/scripts/docker-clean-db.js | 35 ++ package.json | 8 +- server/db/migrations/2.0.0.js | 1 + server/graph/resolvers/group.js | 7 +- server/graph/resolvers/system.js | 7 +- server/graph/schemas/group.graphql | 42 ++- server/graph/schemas/system.graphql | 1 + server/jobs/purge-uploads.js | 2 +- server/master.js | 1 + server/setup.js | 10 + server/views/new.pug | 2 +- 36 files changed, 1116 insertions(+), 958 deletions(-) create mode 100644 client/components/admin/admin-groups-edit-permissions.vue create mode 100644 client/components/admin/admin-groups-edit-rules.vue create mode 100644 client/components/admin/admin-groups-edit-users.vue delete mode 100644 client/components/common/criterias-item.vue delete mode 100644 client/components/common/criterias.vue create mode 100644 client/static/svg/icon-apple-logo.svg create mode 100644 client/static/svg/icon-delete-file.svg create mode 100644 client/static/svg/icon-docker-logo.svg create mode 100644 client/static/svg/icon-linux-logo.svg create mode 100644 client/static/svg/icon-windows-logo.svg create mode 100644 dev/scripts/docker-clean-db.js diff --git a/Makefile b/Makefile index 2ebf8e2e..b1c36aca 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,11 @@ docker-dev-rebuild: ## Rebuild dockerized dev image rm -rf ./node_modules docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . build --no-cache --force-rm +docker-dev-clean: ## Clean DB, redis and data folders + rm -rf ./data + docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . exec db psql --dbname=wiki --username=postgres --command='DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public' + docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . exec redis redis-cli flushall + docker-build: ## Run assets generation build in docker docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . run wiki yarn build docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . down diff --git a/client/components/admin.vue b/client/components/admin.vue index c2dc8bee..6e369b6c 100644 --- a/client/components/admin.vue +++ b/client/components/admin.vue @@ -9,13 +9,13 @@ v-list-tile-title {{ $t('admin:dashboard.title') }} v-divider.my-2 v-subheader.pl-4 {{ $t('admin:nav.site') }} - v-list-tile(to='/general') + v-list-tile(to='/general', v-if='hasPermission(`manage:system`)') v-list-tile-avatar: v-icon widgets v-list-tile-title {{ $t('admin:general.title') }} - v-list-tile(to='/locale') + v-list-tile(to='/locale', v-if='hasPermission(`manage:system`)') v-list-tile-avatar: v-icon language v-list-tile-title {{ $t('admin:locale.title') }} - v-list-tile(to='/navigation') + v-list-tile(to='/navigation', v-if='hasPermission([`manage:system`, `manage:navigation`])') v-list-tile-avatar: v-icon near_me v-list-tile-title {{ $t('admin:navigation.title') }} v-list-tile(to='/pages') @@ -24,7 +24,7 @@ v-list-tile-action v-chip(small, disabled, :color='darkMode ? `grey darken-3-d4` : `grey lighten-4`') .caption.grey--text {{ info.pagesTotal }} - v-list-tile(to='/theme') + v-list-tile(to='/theme', v-if='hasPermission([`manage:system`, `manage:theme`])') v-list-tile-avatar: v-icon palette v-list-tile-title {{ $t('admin:theme.title') }} v-divider.my-2 @@ -41,44 +41,46 @@ v-list-tile-action v-chip(small, disabled, :color='darkMode ? `grey darken-3-d4` : `grey lighten-4`') .caption.grey--text {{ info.usersTotal }} - v-divider.my-2 - v-subheader.pl-4 {{ $t('admin:nav.modules') }} - v-list-tile(to='/auth') - v-list-tile-avatar: v-icon lock_outline - v-list-tile-title {{ $t('admin:auth.title') }} - v-list-tile(to='/editor') - v-list-tile-avatar: v-icon transform - v-list-tile-title {{ $t('admin:editor.title') }} - v-list-tile(to='/logging') - v-list-tile-avatar: v-icon graphic_eq - v-list-tile-title {{ $t('admin:logging.title') }} - v-list-tile(to='/rendering') - v-list-tile-avatar: v-icon system_update_alt - v-list-tile-title {{ $t('admin:rendering.title') }} - v-list-tile(to='/search') - v-list-tile-avatar: v-icon search - v-list-tile-title {{ $t('admin:search.title') }} - v-list-tile(to='/storage') - v-list-tile-avatar: v-icon storage - v-list-tile-title {{ $t('admin:storage.title') }} - v-divider.my-2 - v-subheader.pl-4 {{ $t('admin:nav.system') }} - v-list-tile(to='/api') - v-list-tile-avatar: v-icon call_split - v-list-tile-title {{ $t('admin:api.title') }} - v-list-tile(to='/mail') - v-list-tile-avatar: v-icon email - v-list-tile-title {{ $t('admin:mail.title') }} - v-list-tile(to='/system') - v-list-tile-avatar: v-icon tune - v-list-tile-title {{ $t('admin:system.title') }} - v-list-tile(to='/utilities') - v-list-tile-avatar: v-icon build - v-list-tile-title {{ $t('admin:utilities.title') }} - v-list-tile(to='/dev') - v-list-tile-avatar: v-icon weekend - v-list-tile-title {{ $t('admin:dev.title') }} - v-divider.my-2 + template(v-if='hasPermission(`manage:system`)') + v-divider.my-2 + v-subheader.pl-4 {{ $t('admin:nav.modules') }} + v-list-tile(to='/auth') + v-list-tile-avatar: v-icon lock_outline + v-list-tile-title {{ $t('admin:auth.title') }} + v-list-tile(to='/editor') + v-list-tile-avatar: v-icon transform + v-list-tile-title {{ $t('admin:editor.title') }} + v-list-tile(to='/logging') + v-list-tile-avatar: v-icon graphic_eq + v-list-tile-title {{ $t('admin:logging.title') }} + v-list-tile(to='/rendering') + v-list-tile-avatar: v-icon system_update_alt + v-list-tile-title {{ $t('admin:rendering.title') }} + v-list-tile(to='/search') + v-list-tile-avatar: v-icon search + v-list-tile-title {{ $t('admin:search.title') }} + v-list-tile(to='/storage') + v-list-tile-avatar: v-icon storage + v-list-tile-title {{ $t('admin:storage.title') }} + v-divider.my-2 + template(v-if='hasPermission([`manage:system`, `manage:api`])') + v-subheader.pl-4 {{ $t('admin:nav.system') }} + v-list-tile(to='/api', v-if='hasPermission([`manage:system`, `manage:api`])') + v-list-tile-avatar: v-icon call_split + v-list-tile-title {{ $t('admin:api.title') }} + v-list-tile(to='/mail', v-if='hasPermission(`manage:system`)') + v-list-tile-avatar: v-icon email + v-list-tile-title {{ $t('admin:mail.title') }} + v-list-tile(to='/system', v-if='hasPermission(`manage:system`)') + v-list-tile-avatar: v-icon tune + v-list-tile-title {{ $t('admin:system.title') }} + v-list-tile(to='/utilities', v-if='hasPermission(`manage:system`)') + v-list-tile-avatar: v-icon build + v-list-tile-title {{ $t('admin:utilities.title') }} + v-list-tile(to='/dev', v-if='hasPermission([`manage:system`, `manage:api`])') + v-list-tile-avatar: v-icon weekend + v-list-tile-title {{ $t('admin:dev.title') }} + v-divider.my-2 v-list-tile(to='/contribute') v-list-tile-avatar: v-icon favorite v-list-tile-title {{ $t('admin:contribute.title') }} @@ -91,6 +93,7 @@ diff --git a/client/components/admin/admin-groups-edit-rules.vue b/client/components/admin/admin-groups-edit-rules.vue new file mode 100644 index 00000000..68f72954 --- /dev/null +++ b/client/components/admin/admin-groups-edit-rules.vue @@ -0,0 +1,302 @@ + + + + + diff --git a/client/components/admin/admin-groups-edit-users.vue b/client/components/admin/admin-groups-edit-users.vue new file mode 100644 index 00000000..059f1d99 --- /dev/null +++ b/client/components/admin/admin-groups-edit-users.vue @@ -0,0 +1,147 @@ + + + diff --git a/client/components/admin/admin-groups-edit.vue b/client/components/admin/admin-groups-edit.vue index 007aa1dc..215c9042 100644 --- a/client/components/admin/admin-groups-edit.vue +++ b/client/components/admin/admin-groups-edit.vue @@ -6,130 +6,57 @@ img(src='/svg/icon-social-group.svg', alt='Edit Group', style='width: 80px;') .admin-header-title .headline.blue--text.text--darken-2 Edit Group - .subheading.grey--text {{name}} + .subheading.grey--text {{group.name}} v-spacer .caption.grey--text ID #[strong {{group.id}}] v-divider.mx-3(vertical) - v-btn(color='indigo', large, outline, to='/groups') + v-btn(color='grey', large, outline, to='/groups') v-icon arrow_back v-dialog(v-model='deleteGroupDialog', max-width='500', v-if='!group.isSystem') v-btn(color='red', large, outline, slot='activator') v-icon(color='red') delete v-card .dialog-header.is-red Delete Group? - v-card-text Are you sure you want to delete group #[strong {{ name }}]? All users will be unassigned from this group. + v-card-text Are you sure you want to delete group #[strong {{ group.name }}]? All users will be unassigned from this group. v-card-actions v-spacer v-btn(flat, @click='deleteGroupDialog = false') Cancel v-btn(color='red', dark, @click='deleteGroup') Delete - v-btn(color='primary', large, depressed, @click='updateGroup') + v-btn(color='success', large, depressed, @click='updateGroup') v-icon(left) check span Update Group v-card.mt-3 v-tabs(v-model='tab', :color='$vuetify.dark ? "primary" : "grey darken-2"', fixed-tabs, slider-color='white', show-arrows, dark) - v-tab(key='properties') Properties v-tab(key='permissions') Permissions v-tab(key='rules') Page Rules v-tab(key='users') Users - v-tab-item(key='properties', :transition='false', :reverse-transition='false') - v-card - v-card-text - v-text-field( - outline - background-color='grey lighten-3' - v-model='name' - label='Group Name' - counter='255' - prepend-icon='people' - ) - v-tab-item(key='permissions', :transition='false', :reverse-transition='false') - v-container.pa-3(fluid, grid-list-md) - v-layout(row, wrap) - v-flex(xs12, md6, lg4, v-for='pmGroup in permissions') - v-card.md2.grey(flat, :class='$vuetify.dark ? "darken-4" : "lighten-5"') - v-subheader {{pmGroup.category}} - v-card-text.pt-0 - template(v-for='(pm, idx) in pmGroup.items') - v-checkbox.pt-0( - :key='pm.permission' - :label='pm.permission' - :hint='pm.hint' - persistent-hint - color='primary' - v-model='group.permissions' - :value='pm.permission' - :append-icon='pm.warning ? "warning" : null', - :disabled='(group.isSystem && pm.restrictedForSystem) || group.id === 1 || pm.disabled' - ) - v-divider.mt-3(v-if='idx < pmGroup.items.length - 1') + group-permissions(v-model='group', @refresh='refresh') v-tab-item(key='rules', :transition='false', :reverse-transition='false') - v-card - v-card-title.pb-0 - v-spacer - v-btn(flat, outline) - v-icon(left) arrow_drop_down - | Load Preset - v-btn(flat, outline) - v-icon(left) vertical_align_bottom - | Import Rules - .pa-3.pl-4 - criterias + group-rules(v-model='group', @refresh='refresh') v-tab-item(key='users', :transition='false', :reverse-transition='false') - v-card - v-card-title.pb-0 - v-spacer - v-btn(color='primary', outline, flat, @click='searchUserDialog = true') - v-icon(left) assignment_ind - | Assign User - v-data-table( - :items='group.users', - :headers='headers', - :search='search', - :pagination.sync='pagination', - :rows-per-page-items='[15]' - hide-actions - ) - template(slot='items', slot-scope='props') - tr(:active='props.selected') - td.text-xs-right {{ props.item.id }} - td {{ props.item.name }} - td {{ props.item.email }} - td - v-menu(bottom, right, min-width='200') - v-btn(icon, slot='activator'): v-icon.grey--text.text--darken-1 more_horiz - v-list - v-list-tile(@click='unassignUser(props.item.id)') - v-list-tile-action: v-icon(color='orange') highlight_off - v-list-tile-content - v-list-tile-title Unassign - template(slot='no-data') - v-alert.ma-3(icon='warning', :value='true', outline) No users to display. - .text-xs-center.py-2(v-if='users.length > 15') - v-pagination(v-model='pagination.page', :length='pages') - - user-search(v-model='searchUserDialog', @select='assignUser') + group-users(v-model='group', @refresh='refresh') diff --git a/client/components/common/criterias.vue b/client/components/common/criterias.vue deleted file mode 100644 index 4c5a03e7..00000000 --- a/client/components/common/criterias.vue +++ /dev/null @@ -1,173 +0,0 @@ - - - - - diff --git a/client/components/common/nav-header.vue b/client/components/common/nav-header.vue index 2722cf99..e8c77ee6 100644 --- a/client/components/common/nav-header.vue +++ b/client/components/common/nav-header.vue @@ -16,117 +16,123 @@ :loading='searchIsLoading', @keyup.enter='searchEnter' ) - v-menu(open-on-hover, offset-y, bottom, left, min-width='250') - v-toolbar-side-icon.btn-animate-app(slot='activator') - v-icon view_module - v-list(dense, :light='!$vuetify.dark', :dark='$vuetify.dark', :class='$vuetify.dark ? `grey darken-4` : ``').py-0 - v-list-tile(avatar, href='/') - v-list-tile-avatar: v-icon(color='blue') home - v-list-tile-content Home - v-list-tile(avatar, @click='pageNew') - v-list-tile-avatar: v-icon(color='green') add_box - v-list-tile-content New Page - template(v-if='path && path.length') - v-divider.my-0 - v-subheader Current Page - v-list-tile(avatar, @click='pageView', v-if='mode !== `view`') - v-list-tile-avatar: v-icon(color='indigo') subject - v-list-tile-content View - v-list-tile(avatar, @click='pageEdit', v-if='mode !== `edit`') - v-list-tile-avatar: v-icon(color='indigo') edit - v-list-tile-content Edit - v-list-tile(avatar, @click='pageHistory', v-if='mode !== `history`') - v-list-tile-avatar: v-icon(color='indigo') history - v-list-tile-content History - v-list-tile(avatar, @click='pageSource', v-if='mode !== `source`') - v-list-tile-avatar: v-icon(color='indigo') code - v-list-tile-content View Source - v-list-tile(avatar, @click='pageMove') - v-list-tile-avatar: v-icon(color='indigo') forward - v-list-tile-content Move / Rename - v-list-tile(avatar, @click='pageDelete') - v-list-tile-avatar: v-icon(color='red darken-2') delete - v-list-tile-content Delete - v-divider.my-0 - v-subheader Assets - v-list-tile(avatar, @click='') - v-list-tile-avatar: v-icon(color='blue-grey') burst_mode - v-list-tile-content Images & Files - v-toolbar-title(:class='{ "ml-2": $vuetify.breakpoint.mdAndUp, "ml-0": $vuetify.breakpoint.smAndDown }') - span.subheading {{title}} - v-spacer(v-if='searchIsShown && $vuetify.breakpoint.mdAndUp') - transition(name='navHeaderSearch') - v-text-field( - ref='searchField', - v-if='searchIsShown && $vuetify.breakpoint.mdAndUp', - v-model='search', - clearable, - color='white', - label='Search...', - single-line, - solo - flat - hide-details, - prepend-inner-icon='search', - :loading='searchIsLoading', - @keyup.enter='searchEnter' - ) - v-progress-linear( - indeterminate, - slot='progress', - height='2', - color='blue' - ) - v-spacer - .navHeaderLoading.mr-3 - v-progress-circular(indeterminate, color='blue', :size='22', :width='2' v-show='isLoading') - slot(name='actions') - v-btn( - v-if='!hideSearch && $vuetify.breakpoint.smAndDown' - @click='searchToggle' - icon - ) - v-icon(color='grey') search - v-tooltip(bottom, v-if='isAuthenticated && isAdmin') - v-btn.btn-animate-rotate(icon, href='/a', slot='activator') - v-icon(color='grey') settings - span Admin - v-menu(offset-y, min-width='300') - v-tooltip(bottom, slot='activator') - v-btn.btn-animate-grow(icon, slot='activator', outline, :color='isAuthenticated ? `blue` : `grey darken-3`') - v-icon(color='grey') account_circle - span Account - v-list.py-0 - template(v-if='isAuthenticated') - v-list-tile.py-3.grey(avatar, :class='$vuetify.dark ? `darken-4-l5` : `lighten-5`') - v-list-tile-avatar - v-avatar.blue(v-if='picture.kind === `initials`', :size='40') - span.white--text.subheading {{picture.initials}} - v-avatar(v-else-if='picture.kind === `image`', :size='40') - v-img(:src='picture.url') - v-list-tile-content - v-list-tile-title {{name}} - v-list-tile-sub-title {{email}} - v-divider.my-0 - v-list-tile(href='/w') - v-list-tile-action: v-icon(color='blue') web - v-list-tile-title My Wiki - v-divider.my-0 - v-list-tile(href='/p') - v-list-tile-action: v-icon(color='blue') person - v-list-tile-title Profile - v-divider.my-0 - v-list-tile(@click='logout') - v-list-tile-action: v-icon(color='red') exit_to_app - v-list-tile-title Logout - template(v-else) - v-list-tile(href='/login') - v-list-tile-action: v-icon(color='grey') person - v-list-tile-title Login - v-divider.my-0 - v-list-tile(href='/register') - v-list-tile-action: v-icon(color='grey') person_add - v-list-tile-title Register + v-layout(row) + v-flex(xs6, :md4='searchIsShown', :md6='!searchIsShown') + v-toolbar.nav-header-inner(color='black', dark, flat) + v-menu(open-on-hover, offset-y, bottom, left, min-width='250') + v-toolbar-side-icon.btn-animate-app(slot='activator') + v-icon view_module + v-list(dense, :light='!$vuetify.dark', :dark='$vuetify.dark', :class='$vuetify.dark ? `grey darken-4` : ``').py-0 + v-list-tile(avatar, href='/') + v-list-tile-avatar: v-icon(color='blue') home + v-list-tile-content Home + v-list-tile(avatar, @click='pageNew') + v-list-tile-avatar: v-icon(color='green') add_box + v-list-tile-content New Page + template(v-if='path && path.length') + v-divider.my-0 + v-subheader Current Page + v-list-tile(avatar, @click='pageView', v-if='mode !== `view`') + v-list-tile-avatar: v-icon(color='indigo') subject + v-list-tile-content View + v-list-tile(avatar, @click='pageEdit', v-if='mode !== `edit`') + v-list-tile-avatar: v-icon(color='indigo') edit + v-list-tile-content Edit + v-list-tile(avatar, @click='pageHistory', v-if='mode !== `history`') + v-list-tile-avatar: v-icon(color='indigo') history + v-list-tile-content History + v-list-tile(avatar, @click='pageSource', v-if='mode !== `source`') + v-list-tile-avatar: v-icon(color='indigo') code + v-list-tile-content View Source + v-list-tile(avatar, @click='pageMove') + v-list-tile-avatar: v-icon(color='indigo') forward + v-list-tile-content Move / Rename + v-list-tile(avatar, @click='pageDelete') + v-list-tile-avatar: v-icon(color='red darken-2') delete + v-list-tile-content Delete + v-divider.my-0 + v-subheader Assets + v-list-tile(avatar, @click='') + v-list-tile-avatar: v-icon(color='blue-grey') burst_mode + v-list-tile-content Images & Files + v-toolbar-title(:class='{ "ml-2": $vuetify.breakpoint.mdAndUp, "ml-0": $vuetify.breakpoint.smAndDown }') + span.subheading {{title}} + v-flex(md4, v-if='searchIsShown && $vuetify.breakpoint.mdAndUp') + v-toolbar.nav-header-inner(color='black', dark, flat) + transition(name='navHeaderSearch') + v-text-field( + ref='searchField', + v-if='searchIsShown && $vuetify.breakpoint.mdAndUp', + v-model='search', + clearable, + color='white', + label='Search...', + single-line, + solo + flat + hide-details, + prepend-inner-icon='search', + :loading='searchIsLoading', + @keyup.enter='searchEnter' + ) + v-progress-linear( + indeterminate, + slot='progress', + height='2', + color='blue' + ) + v-flex(xs6, :md4='searchIsShown', :md6='!searchIsShown') + v-toolbar.nav-header-inner(color='black', dark, flat) + v-spacer + .navHeaderLoading.mr-3 + v-progress-circular(indeterminate, color='blue', :size='22', :width='2' v-show='isLoading') + slot(name='actions') + v-btn( + v-if='!hideSearch && $vuetify.breakpoint.smAndDown' + @click='searchToggle' + icon + ) + v-icon(color='grey') search + v-tooltip(bottom, v-if='isAuthenticated && isAdmin') + v-btn.btn-animate-rotate(icon, href='/a', slot='activator') + v-icon(color='grey') settings + span Admin + v-menu(offset-y, min-width='300') + v-tooltip(bottom, slot='activator') + v-btn.btn-animate-grow(icon, slot='activator', outline, :color='isAuthenticated ? `blue` : `grey darken-3`') + v-icon(color='grey') account_circle + span Account + v-list.py-0 + template(v-if='isAuthenticated') + v-list-tile.py-3.grey(avatar, :class='$vuetify.dark ? `darken-4-l5` : `lighten-5`') + v-list-tile-avatar + v-avatar.blue(v-if='picture.kind === `initials`', :size='40') + span.white--text.subheading {{picture.initials}} + v-avatar(v-else-if='picture.kind === `image`', :size='40') + v-img(:src='picture.url') + v-list-tile-content + v-list-tile-title {{name}} + v-list-tile-sub-title {{email}} + v-divider.my-0 + v-list-tile(href='/w') + v-list-tile-action: v-icon(color='blue') web + v-list-tile-title My Wiki + v-divider.my-0 + v-list-tile(href='/p') + v-list-tile-action: v-icon(color='blue') person + v-list-tile-title Profile + v-divider.my-0 + v-list-tile(@click='logout') + v-list-tile-action: v-icon(color='red') exit_to_app + v-list-tile-title Logout + template(v-else) + v-list-tile(href='/login') + v-list-tile-action: v-icon(color='grey') person + v-list-tile-title Login + v-divider.my-0 + v-list-tile(href='/register') + v-list-tile-action: v-icon(color='grey') person_add + v-list-tile-title Register page-selector(mode='create', v-model='newPageModal', :open-handler='pageNewCreate') @@ -251,6 +257,12 @@ export default { padding-right: 14px; } } + + &-inner { + .v-toolbar__content { + padding: 0; + } + } } .navHeaderSearch { diff --git a/client/components/common/user-search.vue b/client/components/common/user-search.vue index 88c6276d..174ec986 100644 --- a/client/components/common/user-search.vue +++ b/client/components/common/user-search.vue @@ -3,7 +3,7 @@ v-model='dialogOpen' max-width='650' ) - v-card + v-card.wiki-form .dialog-header span Search User v-spacer @@ -16,23 +16,25 @@ ) v-card-text v-text-field( - solo - flat + outline label='Search Users...' v-model='search' - prepend-icon='search' - :background-color='$vuetify.dark ? "grey darken-4" : "blue lighten-5"' + prepend-inner-icon='search' color='primary' ref='searchIpt' hide-details ) - v-list(two-line) + v-list.grey.mt-3.py-0.radius-7( + :class='$vuetify.dark ? `darken-3-d5` : `lighten-3`' + two-line + dense + ) template(v-for='(usr, idx) in items') v-list-tile(:key='usr.id', @click='setUser(usr.id)') v-list-tile-avatar(size='40', color='primary') span.body-1.white--text {{usr.name | initials}} v-list-tile-content - v-list-tile-title {{usr.name}} + v-list-tile-title.body-2 {{usr.name}} v-list-tile-sub-title {{usr.email}} v-list-tile-action v-icon(color='primary') arrow_forward diff --git a/client/components/editor.vue b/client/components/editor.vue index d7c4efb0..5a09d9e5 100644 --- a/client/components/editor.vue +++ b/client/components/editor.vue @@ -243,6 +243,7 @@ export default { }) this.$store.set('editor/id', _.get(resp, 'page.id')) this.$store.set('editor/mode', 'update') + window.location.assign(`/${this.$store.get('page/path')}`) } else { throw new Error(_.get(resp, 'responseResult.message')) } diff --git a/client/graph/admin/groups/groups-mutation-update.gql b/client/graph/admin/groups/groups-mutation-update.gql index fabee96a..dd614a73 100644 --- a/client/graph/admin/groups/groups-mutation-update.gql +++ b/client/graph/admin/groups/groups-mutation-update.gql @@ -1,6 +1,6 @@ -mutation ($id: Int!, $name: String!) { +mutation ($id: Int!, $name: String!, $permissions: [String]!, $pageRules: [PageRuleInput]!) { groups { - update(id: $id, name: $name) { + update(id: $id, name: $name, permissions: $permissions, pageRules: $pageRules) { responseResult { succeeded errorCode diff --git a/client/graph/admin/groups/groups-query-single.gql b/client/graph/admin/groups/groups-query-single.gql index 43a65fc7..69f1237e 100644 --- a/client/graph/admin/groups/groups-query-single.gql +++ b/client/graph/admin/groups/groups-query-single.gql @@ -8,9 +8,10 @@ query ($id: Int!) { pageRules { id path - role - exact - allow + roles + match + deny + locales } users { id diff --git a/client/graph/admin/system/system-query-info.gql b/client/graph/admin/system/system-query-info.gql index f9c6d5b4..d38aace0 100644 --- a/client/graph/admin/system/system-query-info.gql +++ b/client/graph/admin/system/system-query-info.gql @@ -9,6 +9,7 @@ query { latestVersion latestVersionReleaseDate operatingSystem + platform hostname cpuCores ramTotal diff --git a/client/static/svg/icon-apple-logo.svg b/client/static/svg/icon-apple-logo.svg new file mode 100644 index 00000000..738c1d02 --- /dev/null +++ b/client/static/svg/icon-apple-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/static/svg/icon-delete-file.svg b/client/static/svg/icon-delete-file.svg new file mode 100644 index 00000000..1e6466e9 --- /dev/null +++ b/client/static/svg/icon-delete-file.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/client/static/svg/icon-docker-logo.svg b/client/static/svg/icon-docker-logo.svg new file mode 100644 index 00000000..86b6d9b0 --- /dev/null +++ b/client/static/svg/icon-docker-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/static/svg/icon-linux-logo.svg b/client/static/svg/icon-linux-logo.svg new file mode 100644 index 00000000..ce9ee737 --- /dev/null +++ b/client/static/svg/icon-linux-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/static/svg/icon-windows-logo.svg b/client/static/svg/icon-windows-logo.svg new file mode 100644 index 00000000..0fda6958 --- /dev/null +++ b/client/static/svg/icon-windows-logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/themes/default/components/page.vue b/client/themes/default/components/page.vue index faddd3e3..9f6c40db 100644 --- a/client/themes/default/components/page.vue +++ b/client/themes/default/components/page.vue @@ -6,16 +6,28 @@ dark app clipped - :mini-variant='$vuetify.breakpoint.md || $vuetify.breakpoint.sm' - mini-variant-width='80' mobile-break-point='600' - :temporary='$vuetify.breakpoint.xs' + :temporary='$vuetify.breakpoint.mdAndDown' v-model='navShown' ) vue-scroll(:ops='scrollStyle') nav-sidebar(:color='darkMode ? `grey darken-3` : `primary`') slot(name='sidebar') + v-fab-transition + v-btn( + fab + color='primary' + fixed + bottom + left + small + @click='navShown = !navShown' + v-if='$vuetify.breakpoint.mdAndDown' + v-show='!navShown' + ) + v-icon menu + v-content template(v-if='path !== `home`') v-toolbar(:color='darkMode ? `grey darken-4-d3` : `grey lighten-3`', flat, dense) @@ -167,7 +179,8 @@ export default { }, data() { return { - navOpen: false, + navShown: false, + navExpanded: false, upBtnShown: false, scrollOpts: { duration: 1500, @@ -203,10 +216,6 @@ export default { }, computed: { darkMode: get('site/dark'), - navShown: { - get() { return this.navOpen || this.$vuetify.breakpoint.smAndUp }, - set(val) { this.navOpen = val } - }, rating: { get () { return 3.5 @@ -232,6 +241,7 @@ export default { }, mounted () { Prism.highlightAllUnder(this.$refs.container) + this.navShown = this.$vuetify.breakpoint.smAndUp }, methods: { toggleNavigation () { diff --git a/dev/scripts/docker-clean-db.js b/dev/scripts/docker-clean-db.js new file mode 100644 index 00000000..d2b3c8ad --- /dev/null +++ b/dev/scripts/docker-clean-db.js @@ -0,0 +1,35 @@ +const { Client } = require('pg') +const fs = require('fs') +const path = require('path') +const yaml = require('js-yaml') + +let config = {} + +try { + conf = yaml.safeLoad( + cfgHelper.parseConfigValue( + fs.readFileSync(path.join(process.cwd(), 'dev/docker/config.yml'), 'utf8') + ) + ) +} catch (err) { + console.error(err.message) + process.exit(1) +} + +const client = new Client({ + user: config.db.username, + host: config.db.host, + database: config.db.database, + password: config.db.password, + port: config.db.port, +}) + +async function main () { + await client.connect() + await client.query('DROP SCHEMA public CASCADE;') + await client.query('CREATE SCHEMA public;') + await client.end() + console.info('Success.') +} + +main() diff --git a/package.json b/package.json index 800931dd..be07fbc1 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,7 @@ "dev": "node wiki dev", "build": "webpack --profile --config dev/webpack/webpack.prod.js", "watch": "webpack --config dev/webpack/webpack.dev.js", - "test": "eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest", - "docker:dev:up": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . up -d && docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . exec wiki yarn dev", - "docker:dev:down": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . down", - "docker:dev:rebuild": "rmdir node_modules /s /q && docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . build --no-cache --force-rm", - "docker:build": "docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . run wiki yarn build && docker-compose -f ./dev/docker/docker-compose.yml -p wiki --project-directory . down" + "test": "eslint --format codeframe --ext .js,.vue . && pug-lint server/views && jest" }, "bin": { "wiki": "wiki.js" @@ -39,7 +35,7 @@ }, "homepage": "https://github.com/Requarks/wiki#readme", "engines": { - "node": ">=10.10" + "node": ">=10.12" }, "dependencies": { "apollo-server": "2.2.2", diff --git a/server/db/migrations/2.0.0.js b/server/db/migrations/2.0.0.js index 7a1038b6..49b92a14 100644 --- a/server/db/migrations/2.0.0.js +++ b/server/db/migrations/2.0.0.js @@ -56,6 +56,7 @@ exports.up = knex => { table.increments('id').primary() table.string('name').notNullable() table.json('permissions').notNullable() + table.json('pageRules').notNullable() table.boolean('isSystem').notNullable().defaultTo(false) table.string('createdAt').notNullable() table.string('updatedAt').notNullable() diff --git a/server/graph/resolvers/group.js b/server/graph/resolvers/group.js index 962e002c..9e6798f6 100644 --- a/server/graph/resolvers/group.js +++ b/server/graph/resolvers/group.js @@ -41,6 +41,7 @@ module.exports = { const group = await WIKI.models.groups.query().insertAndFetch({ name: args.name, permissions: JSON.stringify(WIKI.data.groups.defaultPermissions), + pageRules: JSON.stringify([]), isSystem: false }) return { @@ -69,7 +70,11 @@ module.exports = { } }, async update(obj, args) { - await WIKI.models.groups.query().patch({ name: args.name }).where('id', args.id) + await WIKI.models.groups.query().patch({ + name: args.name, + permissions: JSON.stringify(args.permissions), + pageRules: JSON.stringify(args.pageRules) + }).where('id', args.id) return { responseResult: graphHelper.generateSuccess('Group has been updated.') } diff --git a/server/graph/resolvers/system.js b/server/graph/resolvers/system.js index 8b099376..795f4651 100644 --- a/server/graph/resolvers/system.js +++ b/server/graph/resolvers/system.js @@ -77,11 +77,14 @@ module.exports = { const osInfo = await getos() osLabel = `${os.type()} - ${osInfo.dist} (${osInfo.codename || os.platform()}) ${osInfo.release || os.release()} ${os.arch()}` } + return osLabel + }, + async platform () { const isDockerized = await fs.pathExists('/.dockerenv') if (isDockerized) { - osLabel = `${osLabel} (Docker Container)` + return 'docker' } - return osLabel + return os.platform() }, hostname() { return os.hostname() diff --git a/server/graph/schemas/group.graphql b/server/graph/schemas/group.graphql index 6db98f7c..5f495e8f 100644 --- a/server/graph/schemas/group.graphql +++ b/server/graph/schemas/group.graphql @@ -37,6 +37,8 @@ type GroupMutation { update( id: Int! name: String! + permissions: [String]! + pageRules: [PageRuleInput]! ): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"]) delete( @@ -77,8 +79,46 @@ type Group { name: String! isSystem: Boolean! permissions: [String]! - pageRules: [Right] + pageRules: [PageRule] users: [UserMinimal] createdAt: Date! updatedAt: Date! } + +type PageRule { + id: String! + deny: Boolean! + match: PageRuleMatch! + roles: [PageRuleRole]! + path: String! + locales: [String]! +} + +input PageRuleInput { + id: String! + deny: Boolean! + match: PageRuleMatch! + roles: [PageRuleRole]! + path: String! + locales: [String]! +} + +enum PageRuleRole { + READ + WRITE + MANAGE + DELETE + AS_READ + AS_WRITE + AS_MANAGE + CM_READ + CM_WRITE + CM_MANAGE +} + +enum PageRuleMatch { + START + EXACT + END + REGEX +} diff --git a/server/graph/schemas/system.graphql b/server/graph/schemas/system.graphql index 91e6563a..804128da 100644 --- a/server/graph/schemas/system.graphql +++ b/server/graph/schemas/system.graphql @@ -44,6 +44,7 @@ type SystemInfo { nodeVersion: String operatingSystem: String pagesTotal: Int + platform: String ramTotal: String redisHost: String redisTotalRAM: String diff --git a/server/jobs/purge-uploads.js b/server/jobs/purge-uploads.js index 53f32877..61a8d450 100644 --- a/server/jobs/purge-uploads.js +++ b/server/jobs/purge-uploads.js @@ -11,7 +11,7 @@ module.exports = async (job) => { WIKI.logger.info('Purging orphaned upload files...') try { - const uplTempPath = path.resolve(process.cwd(), WIKI.config.paths.data, 'temp-upload') + const uplTempPath = path.resolve(process.cwd(), WIKI.config.paths.data, 'uploads') const ls = await fs.readdirAsync(uplTempPath) const fifteenAgo = moment().subtract(15, 'minutes') diff --git a/server/master.js b/server/master.js index 768229f0..818bd3f8 100644 --- a/server/master.js +++ b/server/master.js @@ -153,6 +153,7 @@ module.exports = async () => { app.use((err, req, res, next) => { res.status(err.status || 500) + res.locals.pageMeta.title = 'Error' res.render('error', { message: err.message, error: WIKI.IS_DEBUG ? err : {} diff --git a/server/setup.js b/server/setup.js index a3ac3994..e52e401a 100644 --- a/server/setup.js +++ b/server/setup.js @@ -26,6 +26,7 @@ module.exports = () => { const cfgHelper = require('./helpers/config') const crypto = Promise.promisifyAll(require('crypto')) const pem2jwk = require('pem-jwk').pem2jwk + const semver = require('semver') // ---------------------------------------- // Define Express App @@ -83,6 +84,11 @@ module.exports = () => { WIKI.telemetry.sendEvent('setup', 'finalize') try { + // Basic checks + if (!semver.satisfies(process.version, '>=10.14')) { + throw new Error('Node.js 10.14.x or later required!') + } + // Upgrade from WIKI.js 1.x? if (req.body.upgrade) { await WIKI.system.upgradeFromMongo({ @@ -205,11 +211,15 @@ module.exports = () => { const adminGroup = await WIKI.models.groups.query().insert({ name: 'Administrators', permissions: JSON.stringify(['manage:system']), + pageRules: [], isSystem: true }) const guestGroup = await WIKI.models.groups.query().insert({ name: 'Guests', permissions: JSON.stringify(['read:pages']), + pageRules: [ + { id: 'guest', roles: ['READ', 'AS_READ', 'CM_READ'], match: 'START', deny: false, path: '', locales: [] } + ], isSystem: true }) diff --git a/server/views/new.pug b/server/views/new.pug index 15f13e60..58ad48f6 100644 --- a/server/views/new.pug +++ b/server/views/new.pug @@ -5,7 +5,7 @@ block body v-app .newpage .newpage-content - img.animated.fadeIn(src='/svg/icon-close-window.svg', alt='Henry') + img.animated.fadeIn(src='/svg/icon-delete-file.svg', alt='Not Found') .headline= t('newpage.title') .subheading.mt-3= t('newpage.subtitle') v-btn.mt-5(href='/e' + pagePath, large)