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 @@
+
+ v-card.wiki-form
+ v-card-text(v-if='group.id === 1')
+ v-alert.radius-7(
+ :class='$vuetify.dark ? "grey darken-4" : "orange lighten-5"'
+ color='orange darken-2'
+ outline
+ :value='true'
+ icon='lock_outline'
+ ) This group has access to everything.
+ template(v-else)
+ v-card-title(:class='$vuetify.dark ? `grey darken-3-d5` : `grey lighten-5`')
+ v-alert.radius-7(
+ :class='$vuetify.dark ? `grey darken-3-d3` : `white`'
+ :value='true'
+ color='grey'
+ outline
+ icon='info'
+ ) You must enable global content permissions (under Permissions tab) for page rules to have any effect.
+ v-spacer
+ v-btn(depressed, color='primary', @click='addRule')
+ v-icon(left) add
+ | Add Rule
+ v-menu(
+ right
+ offset-y
+ nudge-left='115'
+ )
+ v-btn.is-icon(slot='activator', flat, outline, color='primary')
+ v-icon more_horiz
+ v-list(dense)
+ v-list-tile(@click='comingSoon')
+ v-list-tile-avatar
+ v-icon keyboard_capslock
+ v-list-tile-title Load Preset
+ v-divider
+ v-list-tile(@click='comingSoon')
+ v-list-tile-avatar
+ v-icon publish
+ v-list-tile-title Save As Preset
+ v-divider
+ v-list-tile(@click='comingSoon')
+ v-list-tile-avatar
+ v-icon cloud_upload
+ v-list-tile-title Import Rules
+ v-divider
+ v-list-tile(@click='comingSoon')
+ v-list-tile-avatar
+ v-icon cloud_download
+ v-list-tile-title Export Rules
+ v-card-text(:class='$vuetify.dark ? `grey darken-4-l5` : `white`')
+ .rules
+ .caption(v-if='group.pageRules.length === 0')
+ em(:class='$vuetify.dark ? `grey--text` : `blue-grey--text`') This group has no page rules yet.
+ .rule(v-for='rule of group.pageRules', :key='rule.id')
+ v-btn.ma-0.rule-deny-btn(
+ solo
+ :color='rule.deny ? "red" : "green"'
+ dark
+ @click='rule.deny = !rule.deny'
+ )
+ v-icon(v-if='rule.deny') block
+ v-icon(v-else) check_circle
+ //- Roles
+ v-select.ml-1(
+ solo
+ :items='roles'
+ v-model='rule.roles'
+ placeholder='Select Role(s)...'
+ hide-details
+ multiple
+ chips
+ deletable-chips
+ small-chips
+ style='flex: 0 1 440px;'
+ :menu-props='{ "maxHeight": 500 }'
+ clearable
+ dense
+ )
+ template(slot='selection', slot-scope='{ item, index }')
+ v-chip.white--text.ml-0(v-if='index <= 2', small, label, :color='rule.deny ? `red` : `green`').caption {{ item.value }}
+ v-chip.white--text.ml-0(v-if='index === 3', small, label, :color='rule.deny ? `red lighten-2` : `green lighten-2`').caption + {{ rule.roles.length - 3 }} more
+ template(slot='item', slot-scope='props')
+ v-list-tile-action(style='min-width: 30px;')
+ v-checkbox(
+ v-model='props.tile.props.value'
+ hide-details
+ color='primary'
+ )
+ v-icon.mr-2(:color='rule.deny ? `red` : `green`') {{props.item.icon}}
+ v-list-tile-content
+ v-list-tile-title.body-2 {{props.item.text}}
+ v-chip.mr-2.grey--text(label, small, :color='$vuetify.dark ? `grey darken-4` : `grey lighten-4`').caption {{props.item.value}}
+
+ //- Match
+ v-select.ml-1.mr-1(
+ solo
+ :items='matches'
+ v-model='rule.match'
+ placeholder='Match...'
+ hide-details
+ style='flex: 0 1 250px;'
+ dense
+ )
+ template(slot='selection', slot-scope='{ item, index }')
+ .body-1 {{item.text}}
+ template(slot='item', slot-scope='data')
+ v-list-tile-avatar
+ v-avatar.white--text.radius-4(color='blue', size='30', tile) {{ data.item.icon }}
+ v-list-tile-content
+ v-list-tile-title(v-html='data.item.text')
+ //- Locales
+ v-select.mr-1(
+ :background-color='$vuetify.dark ? `grey darken-3-d5` : `blue-grey lighten-5`'
+ solo
+ :items='locales'
+ v-model='rule.locales'
+ placeholder='Any Locale'
+ multiple
+ hide-details
+ dense
+ :menu-props='{ "minWidth": 250 }'
+ style='flex: 0 1 150px;'
+ )
+ template(slot='selection', slot-scope='{ item, index }')
+ v-chip.white--text.ml-0(v-if='rule.locales.length === 1', small, label, :color='rule.deny ? `red` : `green`').caption {{ item.value.toUpperCase() }}
+ v-chip.white--text.ml-0(v-else-if='index === 0', small, label, :color='rule.deny ? `red` : `green`').caption {{ rule.locales.length }} locales
+ v-list-tile(slot='prepend-item', @click='rule.locales = []')
+ v-list-tile-action(style='min-width: 30px;')
+ v-checkbox(
+ :input-value='rule.locales.length === 0'
+ hide-details
+ color='primary'
+ readonly
+ )
+ v-icon.mr-2(:color='rule.deny ? `red` : `green`') public
+ v-list-tile-content
+ v-list-tile-title.body-2 Any Locale
+ v-divider(slot='prepend-item')
+ template(slot='item', slot-scope='props')
+ v-list-tile-action(style='min-width: 30px;')
+ v-checkbox(
+ v-model='props.tile.props.value'
+ hide-details
+ color='primary'
+ )
+ v-icon.mr-2(:color='rule.deny ? `red` : `green`') language
+ v-list-tile-content
+ v-list-tile-title.body-2 {{props.item.text}}
+ v-chip.mr-2.grey--text(label, small, :color='$vuetify.dark ? `grey darken-4` : `grey lighten-4`').caption {{props.item.value.toUpperCase()}}
+
+ //- Path
+ v-text-field(
+ solo
+ v-model='rule.path'
+ label='Path'
+ :prefix='rule.match !== `END` ? `/` : null'
+ :placeholder='rule.match === `REGEX` ? `Regular Expression` : `Path`'
+ :suffix='rule.match === `REGEX` ? `/` : null'
+ hide-details
+ :color='$vuetify.dark ? `grey` : `blue-grey`'
+ )
+
+ v-btn(icon, @click='removeRule(rule.id)')
+ v-icon(:color='$vuetify.dark ? `grey` : `blue-grey`') clear
+
+
+
+
+
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 @@
+
+ v-card.wiki-form
+ v-card-title(:class='$vuetify.dark ? `grey darken-3-d3` : `grey lighten-5`')
+ v-text-field(
+ outline
+ flat
+ prepend-inner-icon='search'
+ v-model='search'
+ label='Search Group Users...'
+ hide-details
+ )
+ v-spacer
+ v-btn(color='primary', depressed, @click='searchUserDialog = true', :disabled='group.id === 2')
+ 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(:to='`/users/` + props.item.id')
+ v-list-tile-action: v-icon(color='primary') person
+ v-list-tile-content
+ v-list-tile-title View User Profile
+ template(v-if='props.item.id !== 2')
+ v-divider
+ 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='group.users.length > 15')
+ v-pagination(v-model='pagination.page', :length='pages')
+
+ user-search(v-model='searchUserDialog', @select='assignUser')
+
+
+
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 @@
-
- .criterias
- transition-group(name='criterias-group', tag='div')
- .criterias-group(v-for='(group, g) in groups', :key='g')
- transition-group(name='criterias-item', tag='div')
- criterias-item(v-for='(item, i) in group', :key='i', :item='item', :group-index='g', :item-index='i', @update='updateItem', @remove='removeItem')
- .criterias-item-more
- v-btn.ml-0(@click='addItem(group)', small, color='blue-grey lighten-2', dark, depressed)
- v-icon(color='white', left) add
- | Add condition
- .criterias-group-more
- v-btn(@click='addGroup', small, color='blue-grey lighten-1', dark, depressed)
- v-icon(color='white', left) add
- | Add condition group
-
-
-
-
-
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)