浏览代码

feat: page Rules access check

Nicolas Giard 6 年之前
父节点
当前提交
7e62c01ed1
共有 34 个文件被更改,包括 581 次插入725 次删除
  1. 39 37
      client/components/admin.vue
  2. 14 3
      client/components/admin/admin-dashboard.vue
  3. 32 12
      client/components/admin/admin-groups-edit-rules.vue
  4. 28 32
      client/components/common/nav-header.vue
  5. 0 6
      client/graph/common/common-locale-query.gql
  6. 8 0
      client/graph/common/common-localization-query-translations.gql
  7. 3 3
      client/modules/localization.js
  8. 1 0
      client/scss/app.scss
  9. 81 0
      client/scss/pages/_notfound.scss
  10. 1 0
      package.json
  11. 6 1
      server/controllers/common.js
  12. 96 9
      server/core/auth.js
  13. 37 37
      server/graph/resolvers/comment.js
  14. 0 46
      server/graph/resolvers/document.js
  15. 44 44
      server/graph/resolvers/file.js
  16. 30 30
      server/graph/resolvers/folder.js
  17. 12 0
      server/graph/resolvers/group.js
  18. 3 0
      server/graph/resolvers/localization.js
  19. 0 9
      server/graph/resolvers/page.js
  20. 0 53
      server/graph/resolvers/right.js
  21. 0 24
      server/graph/resolvers/setting.js
  22. 0 4
      server/graph/resolvers/system.js
  23. 56 56
      server/graph/resolvers/tag.js
  24. 0 12
      server/graph/resolvers/translation.js
  25. 0 1
      server/graph/resolvers/user.js
  26. 14 225
      server/graph/schemas/common.graphql
  27. 2 15
      server/graph/schemas/group.graphql
  28. 6 0
      server/graph/schemas/localization.graphql
  29. 1 26
      server/graph/schemas/page.graphql
  30. 1 1
      server/graph/schemas/site.graphql
  31. 22 30
      server/graph/schemas/system.graphql
  32. 29 7
      server/models/users.js
  33. 2 2
      server/setup.js
  34. 13 0
      server/views/notfound.pug

+ 39 - 37
client/components/admin.vue

@@ -7,40 +7,42 @@
           v-list-tile.pt-2(to='/dashboard')
             v-list-tile-avatar: v-icon dashboard
             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-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-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-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')
-            v-list-tile-avatar: v-icon insert_drive_file
-            v-list-tile-title {{ $t('admin:pages.title') }}
-            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-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
-          v-subheader.pl-4 {{ $t('admin:nav.users') }}
-          v-list-tile(to='/groups')
-            v-list-tile-avatar: v-icon people
-            v-list-tile-title {{ $t('admin:groups.title') }}
-            v-list-tile-action
-              v-chip(small, disabled, :color='darkMode ? `grey darken-3-d4` : `grey lighten-4`')
-                .caption.grey--text {{ info.groupsTotal }}
-          v-list-tile(to='/users')
-            v-list-tile-avatar: v-icon perm_identity
-            v-list-tile-title {{ $t('admin:users.title') }}
-            v-list-tile-action
-              v-chip(small, disabled, :color='darkMode ? `grey darken-3-d4` : `grey lighten-4`')
-                .caption.grey--text {{ info.usersTotal }}
+          template(v-if='hasPermission([`manage:system`, `manage:navigation`, `write:pages`, `manage:pages`, `delete:pages`])')
+            v-divider.my-2
+            v-subheader.pl-4 {{ $t('admin:nav.site') }}
+            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-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-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', v-if='hasPermission([`manage:system`, `write:pages`, `manage:pages`, `delete:pages`])')
+              v-list-tile-avatar: v-icon insert_drive_file
+              v-list-tile-title {{ $t('admin:pages.title') }}
+              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-if='hasPermission([`manage:system`, `manage:theme`])')
+              v-list-tile-avatar: v-icon palette
+              v-list-tile-title {{ $t('admin:theme.title') }}
+          template(v-if='hasPermission([`manage:system`, `manage:groups`, `write:groups`, `manage:users`, `write:users`])')
+            v-divider.my-2
+            v-subheader.pl-4 {{ $t('admin:nav.users') }}
+            v-list-tile(to='/groups', v-if='hasPermission([`manage:system`, `manage:groups`, `write:groups`])')
+              v-list-tile-avatar: v-icon people
+              v-list-tile-title {{ $t('admin:groups.title') }}
+              v-list-tile-action
+                v-chip(small, disabled, :color='darkMode ? `grey darken-3-d4` : `grey lighten-4`')
+                  .caption.grey--text {{ info.groupsTotal }}
+            v-list-tile(to='/users', v-if='hasPermission([`manage:system`, `manage:groups`, `write:groups`, `manage:users`, `write:users`])')
+              v-list-tile-avatar: v-icon perm_identity
+              v-list-tile-title {{ $t('admin:users.title') }}
+              v-list-tile-action
+                v-chip(small, disabled, :color='darkMode ? `grey darken-3-d4` : `grey lighten-4`')
+                  .caption.grey--text {{ info.usersTotal }}
           template(v-if='hasPermission(`manage:system`)')
             v-divider.my-2
             v-subheader.pl-4 {{ $t('admin:nav.modules') }}
@@ -62,8 +64,8 @@
             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-divider.my-2
             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
@@ -74,8 +76,8 @@
             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(to='/utilities', v-if='hasPermission(`manage:system`)', disabled)
+              v-list-tile-avatar: v-icon(color='grey lighten-2') 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

+ 14 - 3
client/components/admin/admin-dashboard.vue

@@ -45,7 +45,7 @@
           :class='isLatestVersion ? "teal lighten-2" : "red lighten-2"'
           dark
           )
-          v-btn(fab, absolute, right, top, small, light, to='system')
+          v-btn(fab, absolute, right, top, small, light, to='system', v-if='hasPermission(`manage:system`)')
             v-icon(v-if='isLatestVersion', color='teal') build
             v-icon(v-else, color='red darken-4') get_app
           v-card-text
@@ -101,6 +101,7 @@
 </template>
 
 <script>
+import _ from 'lodash'
 import AnimatedNumber from 'animated-number-vue'
 import { get } from 'vuex-pathify'
 
@@ -118,10 +119,20 @@ export default {
     isLatestVersion() {
       return this.info.currentVersion === this.info.latestVersion
     },
-    info: get('admin/info')
+    info: get('admin/info'),
+    permissions: get('user/permissions')
   },
   methods: {
-    round(val) { return Math.round(val) }
+    round(val) { return Math.round(val) },
+    hasPermission(prm) {
+      if (_.isArray(prm)) {
+        return _.some(prm, p => {
+          return _.includes(this.permissions, p)
+        })
+      } else {
+        return _.includes(this.permissions, prm)
+      }
+    }
   }
 }
 </script>

+ 32 - 12
client/components/admin/admin-groups-edit-rules.vue

@@ -78,8 +78,8 @@
               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
+                v-chip.white--text.ml-0(v-if='index <= 1', small, label, :color='rule.deny ? `red` : `green`').caption {{ item.value }}
+                v-chip.white--text.ml-0(v-if='index === 2', small, label, :color='rule.deny ? `red lighten-2` : `green lighten-2`').caption + {{ rule.roles.length - 2 }} more
               template(slot='item', slot-scope='props')
                 v-list-tile-action(style='min-width: 30px;')
                   v-checkbox(
@@ -163,6 +163,26 @@
 
             v-btn(icon, @click='removeRule(rule.id)')
               v-icon(:color='$vuetify.dark ? `grey` : `blue-grey`') clear
+
+        v-divider.mt-3
+        v-subheader.pl-0 Rules Order
+        .body-1.pl-3 Rules are applied in order of path specificity. A more precise path will always override a less defined path.
+        .body-1.pl-4 For example, #[span.teal--text /geography/countries] will override #[span.teal--text /geography].
+        .body-1.pl-3.pt-2 When 2 rules have the same specificity, the priority is given from lowest to highest as follows:
+        .body-1.pl-3.pt-1
+          ul
+            li
+              strong Path Starts With...
+              em.caption.pl-1 (lowest)
+            li
+              strong Path Ends With...
+            li
+              strong Path Matches Regex...
+            li
+              strong Path Is Exactly...
+              em.caption.pl-1 (highest)
+        .body-1.pl-3.pt-2 When 2 rules have the same path specificity AND the same match type, #[strong.red--text DENY] will always override an #[strong.green--text ALLOW] rule.
+
 </template>
 
 <script>
@@ -178,16 +198,16 @@ export default {
   data() {
     return {
       roles: [
-        { text: 'Read Pages', value: 'READ', icon: 'insert_drive_file' },
-        { text: 'Create Pages', value: 'WRITE', icon: 'insert_drive_file' },
-        { text: 'Edit + Move Pages', value: 'MANAGE', icon: 'insert_drive_file' },
-        { text: 'Delete Pages', value: 'DELETE', icon: 'insert_drive_file' },
-        { text: 'Read / Use Assets', value: 'AS_READ', icon: 'camera' },
-        { text: 'Upload Assets', value: 'AS_WRITE', icon: 'camera' },
-        { text: 'Edit + Delete Assets', value: 'AS_MANAGE', icon: 'camera' },
-        { text: 'Read Comments', value: 'CM_READ', icon: 'insert_comment' },
-        { text: 'Create Comments', value: 'CM_WRITE', icon: 'insert_comment' },
-        { text: 'Edit + Delete Comments', value: 'CM_MANAGE', icon: 'insert_comment' }
+        { text: 'Read Pages', value: 'read:pages', icon: 'insert_drive_file' },
+        { text: 'Create Pages', value: 'write:pages', icon: 'insert_drive_file' },
+        { text: 'Edit + Move Pages', value: 'manage:pages', icon: 'insert_drive_file' },
+        { text: 'Delete Pages', value: 'delete:pages', icon: 'insert_drive_file' },
+        { text: 'Read / Use Assets', value: 'read:assets', icon: 'camera' },
+        { text: 'Upload Assets', value: 'write:assets', icon: 'camera' },
+        { text: 'Edit + Delete Assets', value: 'manage:assets', icon: 'camera' },
+        { text: 'Read Comments', value: 'read:comments', icon: 'insert_comment' },
+        { text: 'Create Comments', value: 'write:comments', icon: 'insert_comment' },
+        { text: 'Edit + Delete Comments', value: 'manage:comments', icon: 'insert_comment' }
       ],
       matches: [
         { text: 'Path Starts With...', value: 'START', icon: '/...' },

+ 28 - 32
client/components/common/nav-header.vue

@@ -97,42 +97,38 @@
             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-menu(v-if='isAuthenticated', 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-btn.btn-animate-grow(icon, slot='activator', outline, color='blue')
                 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-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
+
+          v-tooltip(v-else, left)
+            v-btn(icon, slot='activator', outline, color='grey darken-3', href='/login')
+              v-icon(color='grey') account_circle
+            span Login
 
     page-selector(mode='create', v-model='newPageModal', :open-handler='pageNewCreate')
 </template>

+ 0 - 6
client/graph/common/common-locale-query.gql

@@ -1,6 +0,0 @@
-query($locale: String!, $namespace: String!) {
-  translations(locale:$locale, namespace:$namespace) {
-    key
-    value
-  }
-}

+ 8 - 0
client/graph/common/common-localization-query-translations.gql

@@ -0,0 +1,8 @@
+query($locale: String!, $namespace: String!) {
+  localization {
+    translations(locale:$locale, namespace:$namespace) {
+      key
+      value
+    }
+  }
+}

+ 3 - 3
client/modules/localization.js

@@ -6,7 +6,7 @@ import _ from 'lodash'
 
 /* global siteConfig, graphQL */
 
-import localeQuery from 'gql/common/common-locale-query.gql'
+import localeQuery from 'gql/common/common-localization-query-translations.gql'
 
 export default {
   VueI18Next,
@@ -28,8 +28,8 @@ export default {
               }
             }).then(resp => {
               let ns = {}
-              if (resp.data.translations.length > 0) {
-                resp.data.translations.forEach(entry => {
+              if (_.get(resp, 'data.localization.translations', []).length > 0) {
+                resp.data.localization.translations.forEach(entry => {
                   _.set(ns, entry.key, entry.value)
                 })
               }

+ 1 - 0
client/scss/app.scss

@@ -23,6 +23,7 @@
 // @import 'node_modules/diff2html/dist/diff2html.min';
 
 @import 'pages/new';
+@import 'pages/notfound';
 @import 'pages/unauthorized';
 @import 'pages/welcome';
 @import 'pages/error';

+ 81 - 0
client/scss/pages/_notfound.scss

@@ -0,0 +1,81 @@
+.notfound {
+  background: linear-gradient(to bottom, darken(mc('red', '900'), 25%) 0%, mc('red', '600') 100%);
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  color: mc('grey', '50');
+
+  &::before {
+    content: '';
+    display:block;
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+    background-image: url('../static/svg/motif-circuit.svg');
+    background-position: center center;
+    background-repeat: repeat;
+    background-size: 200px;
+    z-index: 0;
+    opacity: .75;
+    animation: onboardingBgReveal 80s linear infinite;
+
+    @include keyframes(onboardingBgReveal) {
+      0% {
+        background-position-y: 0;
+      }
+      100% {
+        background-position-y: -2000px;
+      }
+    }
+  }
+
+  &::after {
+    content: '';
+    position: absolute;
+    background-color: transparent;
+    background-image: url('../static/svg/motif-overlay.svg');
+    background-attachment: fixed;
+    background-size: cover;
+    opacity: .5;
+    top: 0;
+    left: 0;
+    width: 100vw;
+    height: 100vh;
+  }
+
+  &-content {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    z-index: 2;
+  }
+
+  img {
+    height: 250px;
+    margin-bottom: 3rem;
+    z-index: 2;
+    animation-duration: 2s;
+
+    @include until($tablet) {
+      height: 200px;
+    }
+  }
+
+  h1 {
+    font-size: 1.5rem;
+    margin-bottom: 1rem;
+    z-index: 2;
+  }
+  h2 {
+    margin-bottom: 3rem;
+    z-index: 2;
+  }
+  .v-btn {
+    z-index: 2;
+  }
+}

+ 1 - 0
package.json

@@ -147,6 +147,7 @@
     "remove-markdown": "0.3.0",
     "request": "2.88.0",
     "request-promise": "4.2.2",
+    "safe-regex": "2.0.1",
     "scim-query-filter-parser": "1.1.0",
     "semver": "5.6.0",
     "serve-favicon": "2.5.0",

+ 6 - 1
server/controllers/common.js

@@ -131,6 +131,7 @@ router.get('/*', async (req, res, next) => {
     if (pageArgs.path === 'home') {
       return res.redirect('/login')
     } else {
+      _.set(res.locals, 'pageMeta.title', 'Unauthorized')
       return res.render('unauthorized', { action: 'view'})
     }
   }
@@ -151,7 +152,11 @@ router.get('/*', async (req, res, next) => {
     res.render('welcome')
   } else {
     _.set(res.locals, 'pageMeta.title', 'Page Not Found')
-    res.status(404).render('new', { pagePath: req.path })
+    if (WIKI.auth.checkAccess(req.user, ['write:pages'], pageArgs)) {
+      res.status(404).render('new', { pagePath: req.path })
+    } else {
+      res.render('notfound', { action: 'view'})
+    }
   }
 })
 

+ 96 - 9
server/core/auth.js

@@ -15,6 +15,7 @@ module.exports = {
   guest: {
     cacheExpiration: moment.utc().subtract(1, 'd')
   },
+  groups: {},
 
   /**
    * Initialize the authentication module
@@ -22,23 +23,27 @@ module.exports = {
   init() {
     this.passport = passport
 
-    passport.serializeUser(function (user, done) {
+    passport.serializeUser((user, done) => {
       done(null, user.id)
     })
 
-    passport.deserializeUser(function (id, done) {
-      WIKI.models.users.query().findById(id).then((user) => {
+    passport.deserializeUser(async (id, done) => {
+      try {
+        const user = await WIKI.models.users.query().findById(id).modifyEager('groups', builder => {
+          builder.select('groups.id', 'permissions')
+        })
         if (user) {
           done(null, user)
         } else {
           done(new Error(WIKI.lang.t('auth:errors:usernotfound')), null)
         }
-        return true
-      }).catch((err) => {
+      } catch (err) {
         done(err, null)
-      })
+      }
     })
 
+    this.reloadGroups()
+
     return this
   },
 
@@ -117,13 +122,14 @@ module.exports = {
             res.cookie('jwt', newToken.token, { expires: moment().add(365, 'days').toDate() })
           }
         } catch (err) {
+          WIKI.logger.warn(err)
           return next()
         }
       }
 
       // JWT is NOT valid, set as guest
       if (!user) {
-        if (WIKI.auth.guest.cacheExpiration ) {
+        if (true || WIKI.auth.guest.cacheExpiration.isSameOrBefore(moment.utc())) {
           WIKI.auth.guest = await WIKI.models.users.getGuestUser()
           WIKI.auth.guest.cacheExpiration = moment.utc().add(1, 'm')
         }
@@ -146,18 +152,99 @@ module.exports = {
    * @param {Array<String>} permissions
    * @param {String|Boolean} path
    */
-  checkAccess(user, permissions = [], path = false) {
+  checkAccess(user, permissions = [], page = false) {
     // System Admin
     if (_.includes(user.permissions, 'manage:system')) {
       return true
     }
 
+    const userPermissions = user.permissions ? user.permissions : user.getGlobalPermissions()
+
     // Check Global Permissions
-    if (_.intersection(user.permissions, permissions).length < 1) {
+    if (_.intersection(userPermissions, permissions).length < 1) {
       return false
     }
 
+    console.info('---------------------')
+
     // Check Page Rules
+    if (path && user.groups) {
+      let checkState = {
+        deny: false,
+        match: false,
+        specificity: ''
+      }
+      user.groups.forEach(grp => {
+        const grpId = _.isObject(grp) ? _.get(grp, 'id', 0) : grp
+        _.get(WIKI.auth.groups, `${grpId}.pageRules`, []).forEach(rule => {
+          console.info(page.path)
+          console.info(rule)
+          switch(rule.match) {
+            case 'START':
+              if (_.startsWith(`/${page.path}`, `/${rule.path}`)) {
+                checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['END', 'REGEX', 'EXACT'] })
+              }
+              break
+            case 'END':
+              if (_.endsWith(page.path, rule.path)) {
+                checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['REGEX', 'EXACT'] })
+              }
+              break
+            case 'REGEX':
+              const reg = new RegExp(rule.path)
+              if (reg.test(page.path)) {
+                checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['EXACT'] })
+              }
+            case 'EXACT':
+              if (`/${page.path}` === `/${rule.path}`) {
+                checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: [] })
+              }
+              break
+          }
+        })
+      })
+
+      console.info('DAKSJDHKASJD')
+      console.info(checkState)
+
+      return (checkState.match && !checkState.deny)
+    }
+
     return false
+  },
+
+  /**
+   * Check and apply Page Rule specificity
+   *
+   * @access private
+   */
+  _applyPageRuleSpecificity ({ rule, checkState, higherPriority = [] }) {
+    if (rule.path.length === checkState.specificity.length) {
+      // Do not override higher priority rules
+      if (_.includes(higherPriority, checkState.match)) {
+        return checkState
+      }
+      // Do not override a previous DENY rule with same match
+      if (rule.match === checkState.match && checkState.deny && !rule.deny) {
+        return checkState
+      }
+    } else if (rule.path.length < checkState.specificity.length) {
+      // Do not override higher specificity rules
+      return checkState
+    }
+
+    return {
+      deny: rule.deny,
+      match: rule.match,
+      specificity: rule.path
+    }
+  },
+
+  /**
+   * Reload Groups from DB
+   */
+  async reloadGroups() {
+    const groupsArray = await WIKI.models.groups.query()
+    this.groups = _.keyBy(groupsArray, 'id')
   }
 }

+ 37 - 37
server/graph/resolvers/comment.js

@@ -2,41 +2,41 @@
 /* global WIKI */
 
 module.exports = {
-  Query: {
-    comments(obj, args, context, info) {
-      return WIKI.models.Comment.findAll({ where: args })
-    }
-  },
-  Mutation: {
-    createComment(obj, args) {
-      return WIKI.models.Comment.create({
-        content: args.content,
-        author: args.userId,
-        document: args.documentId
-      })
-    },
-    deleteComment(obj, args) {
-      return WIKI.models.Comment.destroy({
-        where: {
-          id: args.id
-        },
-        limit: 1
-      })
-    },
-    modifyComment(obj, args) {
-      return WIKI.models.Comment.update({
-        content: args.content
-      }, {
-        where: { id: args.id }
-      })
-    }
-  },
-  Comment: {
-    author(cm) {
-      return cm.getAuthor()
-    },
-    document(cm) {
-      return cm.getDocument()
-    }
-  }
+  // Query: {
+  //   comments(obj, args, context, info) {
+  //     return WIKI.models.Comment.findAll({ where: args })
+  //   }
+  // },
+  // Mutation: {
+  //   createComment(obj, args) {
+  //     return WIKI.models.Comment.create({
+  //       content: args.content,
+  //       author: args.userId,
+  //       document: args.documentId
+  //     })
+  //   },
+  //   deleteComment(obj, args) {
+  //     return WIKI.models.Comment.destroy({
+  //       where: {
+  //         id: args.id
+  //       },
+  //       limit: 1
+  //     })
+  //   },
+  //   modifyComment(obj, args) {
+  //     return WIKI.models.Comment.update({
+  //       content: args.content
+  //     }, {
+  //       where: { id: args.id }
+  //     })
+  //   }
+  // },
+  // Comment: {
+  //   author(cm) {
+  //     return cm.getAuthor()
+  //   },
+  //   document(cm) {
+  //     return cm.getDocument()
+  //   }
+  // }
 }

+ 0 - 46
server/graph/resolvers/document.js

@@ -1,46 +0,0 @@
-
-/* global WIKI */
-
-module.exports = {
-  Query: {
-    documents(obj, args, context, info) {
-      return WIKI.models.Document.findAll({ where: args })
-    }
-  },
-  Mutation: {
-    createDocument(obj, args) {
-      return WIKI.models.Document.create(args)
-    },
-    deleteDocument(obj, args) {
-      return WIKI.models.Document.destroy({
-        where: {
-          id: args.id
-        },
-        limit: 1
-      })
-    },
-    modifyDocument(obj, args) {
-      return WIKI.models.Document.update({
-        title: args.title,
-        subtitle: args.subtitle
-      }, {
-        where: { id: args.id }
-      })
-    },
-    moveDocument(obj, args) {
-      return WIKI.models.Document.update({
-        path: args.path
-      }, {
-        where: { id: args.id }
-      })
-    }
-  },
-  Document: {
-    comments(doc) {
-      return doc.getComments()
-    },
-    tags(doc) {
-      return doc.getTags()
-    }
-  }
-}

+ 44 - 44
server/graph/resolvers/file.js

@@ -4,48 +4,48 @@
 const gql = require('graphql')
 
 module.exports = {
-  Query: {
-    files(obj, args, context, info) {
-      return WIKI.models.File.findAll({ where: args })
-    }
-  },
-  Mutation: {
-    uploadFile(obj, args) {
-      // todo
-      return WIKI.models.File.create(args)
-    },
-    deleteFile(obj, args) {
-      return WIKI.models.File.destroy({
-        where: {
-          id: args.id
-        },
-        limit: 1
-      })
-    },
-    renameFile(obj, args) {
-      return WIKI.models.File.update({
-        filename: args.filename
-      }, {
-        where: { id: args.id }
-      })
-    },
-    moveFile(obj, args) {
-      return WIKI.models.File.findById(args.fileId).then(fl => {
-        if (!fl) {
-          throw new gql.GraphQLError('Invalid File ID')
-        }
-        return WIKI.models.Folder.findById(args.folderId).then(fld => {
-          if (!fld) {
-            throw new gql.GraphQLError('Invalid Folder ID')
-          }
-          return fl.setFolder(fld)
-        })
-      })
-    }
-  },
-  File: {
-    folder(fl) {
-      return fl.getFolder()
-    }
-  }
+  // Query: {
+  //   files(obj, args, context, info) {
+  //     return WIKI.models.File.findAll({ where: args })
+  //   }
+  // },
+  // Mutation: {
+  //   uploadFile(obj, args) {
+  //     // todo
+  //     return WIKI.models.File.create(args)
+  //   },
+  //   deleteFile(obj, args) {
+  //     return WIKI.models.File.destroy({
+  //       where: {
+  //         id: args.id
+  //       },
+  //       limit: 1
+  //     })
+  //   },
+  //   renameFile(obj, args) {
+  //     return WIKI.models.File.update({
+  //       filename: args.filename
+  //     }, {
+  //       where: { id: args.id }
+  //     })
+  //   },
+  //   moveFile(obj, args) {
+  //     return WIKI.models.File.findById(args.fileId).then(fl => {
+  //       if (!fl) {
+  //         throw new gql.GraphQLError('Invalid File ID')
+  //       }
+  //       return WIKI.models.Folder.findById(args.folderId).then(fld => {
+  //         if (!fld) {
+  //           throw new gql.GraphQLError('Invalid Folder ID')
+  //         }
+  //         return fl.setFolder(fld)
+  //       })
+  //     })
+  //   }
+  // },
+  // File: {
+  //   folder(fl) {
+  //     return fl.getFolder()
+  //   }
+  // }
 }

+ 30 - 30
server/graph/resolvers/folder.js

@@ -2,34 +2,34 @@
 /* global WIKI */
 
 module.exports = {
-  Query: {
-    folders(obj, args, context, info) {
-      return WIKI.models.Folder.findAll({ where: args })
-    }
-  },
-  Mutation: {
-    createFolder(obj, args) {
-      return WIKI.models.Folder.create(args)
-    },
-    deleteFolder(obj, args) {
-      return WIKI.models.Folder.destroy({
-        where: {
-          id: args.id
-        },
-        limit: 1
-      })
-    },
-    renameFolder(obj, args) {
-      return WIKI.models.Folder.update({
-        name: args.name
-      }, {
-        where: { id: args.id }
-      })
-    }
-  },
-  Folder: {
-    files(grp) {
-      return grp.getFiles()
-    }
-  }
+  // Query: {
+  //   folders(obj, args, context, info) {
+  //     return WIKI.models.Folder.findAll({ where: args })
+  //   }
+  // },
+  // Mutation: {
+  //   createFolder(obj, args) {
+  //     return WIKI.models.Folder.create(args)
+  //   },
+  //   deleteFolder(obj, args) {
+  //     return WIKI.models.Folder.destroy({
+  //       where: {
+  //         id: args.id
+  //       },
+  //       limit: 1
+  //     })
+  //   },
+  //   renameFolder(obj, args) {
+  //     return WIKI.models.Folder.update({
+  //       name: args.name
+  //     }, {
+  //       where: { id: args.id }
+  //     })
+  //   }
+  // },
+  // Folder: {
+  //   files(grp) {
+  //     return grp.getFiles()
+  //   }
+  // }
 }

+ 12 - 0
server/graph/resolvers/group.js

@@ -1,4 +1,5 @@
 const graphHelper = require('../../helpers/graph')
+const safeRegex = require('safe-regex')
 
 /* global WIKI */
 
@@ -44,6 +45,7 @@ module.exports = {
         pageRules: JSON.stringify([]),
         isSystem: false
       })
+      await WIKI.auth.reloadGroups()
       return {
         responseResult: graphHelper.generateSuccess('Group created successfully.'),
         group
@@ -51,6 +53,7 @@ module.exports = {
     },
     async delete(obj, args) {
       await WIKI.models.groups.query().deleteById(args.id)
+      await WIKI.auth.reloadGroups()
       return {
         responseResult: graphHelper.generateSuccess('Group has been deleted.')
       }
@@ -70,11 +73,20 @@ module.exports = {
       }
     },
     async update(obj, args) {
+      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.')
+      }
+
       await WIKI.models.groups.query().patch({
         name: args.name,
         permissions: JSON.stringify(args.permissions),
         pageRules: JSON.stringify(args.pageRules)
       }).where('id', args.id)
+
+      await WIKI.auth.reloadGroups()
+
       return {
         responseResult: graphHelper.generateSuccess('Group has been updated.')
       }

+ 3 - 0
server/graph/resolvers/localization.js

@@ -31,6 +31,9 @@ module.exports = {
         namespacing: WIKI.config.lang.namespacing,
         namespaces: WIKI.config.lang.namespaces
       }
+    },
+    translations (obj, args, context, info) {
+      return WIKI.lang.getByNamespace(args.locale, args.namespace)
     }
   },
   LocalizationMutation: {

+ 0 - 9
server/graph/resolvers/page.js

@@ -16,15 +16,6 @@ module.exports = {
         offsetPage: args.offsetPage || 0,
         offsetSize: args.offsetSize || 100
       })
-    },
-    async list(obj, args, context, info) {
-      return WIKI.models.pages.query().select(
-        'pages.*',
-        WIKI.models.pages.relatedQuery('users').count().as('userCount')
-      )
-    },
-    async single(obj, args, context, info) {
-      return WIKI.models.pages.query().findById(args.id)
     }
   },
   PageMutation: {

+ 0 - 53
server/graph/resolvers/right.js

@@ -1,53 +0,0 @@
-
-/* global WIKI */
-
-const gql = require('graphql')
-
-module.exports = {
-  Query: {
-    rights(obj, args, context, info) {
-      return WIKI.models.Right.findAll({ where: args })
-    }
-  },
-  Mutation: {
-    addRightToGroup(obj, args) {
-      return WIKI.models.Group.findById(args.groupId).then(grp => {
-        if (!grp) {
-          throw new gql.GraphQLError('Invalid Group ID')
-        }
-        return WIKI.models.Right.create({
-          path: args.path,
-          role: args.role,
-          exact: args.exact,
-          allow: args.allow,
-          group: grp
-        })
-      })
-    },
-    removeRightFromGroup(obj, args) {
-      return WIKI.models.Right.destroy({
-        where: {
-          id: args.rightId
-        },
-        limit: 1
-      })
-    },
-    modifyRight(obj, args) {
-      return WIKI.models.Right.update({
-        path: args.path,
-        role: args.role,
-        exact: args.exact,
-        allow: args.allow
-      }, {
-        where: {
-          id: args.id
-        }
-      })
-    }
-  },
-  Right: {
-    group(rt) {
-      return rt.getGroup()
-    }
-  }
-}

+ 0 - 24
server/graph/resolvers/setting.js

@@ -1,24 +0,0 @@
-
-/* global WIKI */
-
-const _ = require('lodash')
-
-module.exports = {
-  Query: {
-    settings(obj, args, context, info) {
-      return WIKI.models.Setting.findAll({ where: args, raw: true }).then(entries => {
-        return _.map(entries, entry => {
-          entry.config = JSON.stringify(entry.config)
-          return entry
-        })
-      })
-    }
-  },
-  Mutation: {
-    setConfigEntry(obj, args) {
-      return WIKI.models.Setting.update({
-        value: args.value
-      }, { where: { key: args.key } })
-    }
-  }
-}

+ 0 - 4
server/graph/resolvers/system.js

@@ -20,13 +20,9 @@ module.exports = {
   Query: {
     async system() { return {} }
   },
-  Mutation: {
-    async system() { return {} }
-  },
   SystemQuery: {
     async info() { return {} }
   },
-  SystemMutation: { },
   SystemInfo: {
     configFile() {
       return path.join(process.cwd(), 'config.yml')

+ 56 - 56
server/graph/resolvers/tag.js

@@ -4,60 +4,60 @@
 const gql = require('graphql')
 
 module.exports = {
-  Query: {
-    tags(obj, args, context, info) {
-      return WIKI.models.Tag.findAll({ where: args })
-    }
-  },
-  Mutation: {
-    assignTagToDocument(obj, args) {
-      return WIKI.models.Tag.findById(args.tagId).then(tag => {
-        if (!tag) {
-          throw new gql.GraphQLError('Invalid Tag ID')
-        }
-        return WIKI.models.Document.findById(args.documentId).then(doc => {
-          if (!doc) {
-            throw new gql.GraphQLError('Invalid Document ID')
-          }
-          return tag.addDocument(doc)
-        })
-      })
-    },
-    createTag(obj, args) {
-      return WIKI.models.Tag.create(args)
-    },
-    deleteTag(obj, args) {
-      return WIKI.models.Tag.destroy({
-        where: {
-          id: args.id
-        },
-        limit: 1
-      })
-    },
-    removeTagFromDocument(obj, args) {
-      return WIKI.models.Tag.findById(args.tagId).then(tag => {
-        if (!tag) {
-          throw new gql.GraphQLError('Invalid Tag ID')
-        }
-        return WIKI.models.Document.findById(args.documentId).then(doc => {
-          if (!doc) {
-            throw new gql.GraphQLError('Invalid Document ID')
-          }
-          return tag.removeDocument(doc)
-        })
-      })
-    },
-    renameTag(obj, args) {
-      return WIKI.models.Group.update({
-        key: args.key
-      }, {
-        where: { id: args.id }
-      })
-    }
-  },
-  Tag: {
-    documents(tag) {
-      return tag.getDocuments()
-    }
-  }
+  // Query: {
+  //   tags(obj, args, context, info) {
+  //     return WIKI.models.Tag.findAll({ where: args })
+  //   }
+  // },
+  // Mutation: {
+  //   assignTagToDocument(obj, args) {
+  //     return WIKI.models.Tag.findById(args.tagId).then(tag => {
+  //       if (!tag) {
+  //         throw new gql.GraphQLError('Invalid Tag ID')
+  //       }
+  //       return WIKI.models.Document.findById(args.documentId).then(doc => {
+  //         if (!doc) {
+  //           throw new gql.GraphQLError('Invalid Document ID')
+  //         }
+  //         return tag.addDocument(doc)
+  //       })
+  //     })
+  //   },
+  //   createTag(obj, args) {
+  //     return WIKI.models.Tag.create(args)
+  //   },
+  //   deleteTag(obj, args) {
+  //     return WIKI.models.Tag.destroy({
+  //       where: {
+  //         id: args.id
+  //       },
+  //       limit: 1
+  //     })
+  //   },
+  //   removeTagFromDocument(obj, args) {
+  //     return WIKI.models.Tag.findById(args.tagId).then(tag => {
+  //       if (!tag) {
+  //         throw new gql.GraphQLError('Invalid Tag ID')
+  //       }
+  //       return WIKI.models.Document.findById(args.documentId).then(doc => {
+  //         if (!doc) {
+  //           throw new gql.GraphQLError('Invalid Document ID')
+  //         }
+  //         return tag.removeDocument(doc)
+  //       })
+  //     })
+  //   },
+  //   renameTag(obj, args) {
+  //     return WIKI.models.Group.update({
+  //       key: args.key
+  //     }, {
+  //       where: { id: args.id }
+  //     })
+  //   }
+  // },
+  // Tag: {
+  //   documents(tag) {
+  //     return tag.getDocuments()
+  //   }
+  // }
 }

+ 0 - 12
server/graph/resolvers/translation.js

@@ -1,12 +0,0 @@
-
-/* global WIKI */
-
-module.exports = {
-  Query: {
-    translations (obj, args, context, info) {
-      return WIKI.lang.getByNamespace(args.locale, args.namespace)
-    }
-  },
-  Mutation: {},
-  Translation: {}
-}

+ 0 - 1
server/graph/resolvers/user.js

@@ -22,7 +22,6 @@ module.exports = {
     },
     async single(obj, args, context, info) {
       let usr = await WIKI.models.users.query().findById(args.id)
-      console.info(usr)
       usr.password = ''
       usr.tfaSecret = ''
       return usr

+ 14 - 225
server/graph/schemas/common.graphql

@@ -1,37 +1,32 @@
-
-
-# ENUMS
-
-enum FileType {
-  binary
-  image
-}
-
-enum RightRole {
-  read
-  write
-  manage
-}
+# ====================== #
+# Wiki.js GraphQL Schema #
+# ====================== #
 
 # DIRECTIVES
+# ----------
 
 directive @auth(requires: [String]) on QUERY | FIELD_DEFINITION | ARGUMENT_DEFINITION
 
 # TYPES
+# -----
 
+# Generic Key Value Pair
 type KeyValuePair {
   key: String!
   value: String!
 }
+# General Key Value Pair Input
 input KeyValuePairInput {
   key: String!
   value: String!
 }
 
+# Generic Mutation Response
 type DefaultResponse {
   responseResult: ResponseStatus
 }
 
+# Mutation Status
 type ResponseStatus {
   succeeded: Boolean!
   errorCode: Int!
@@ -39,220 +34,14 @@ type ResponseStatus {
   message: String
 }
 
-type Comment {
-  id: Int!
-  createdAt: Date
-  updatedAt: Date
-  content: String
-  document: Document!
-  author: User!
-}
-
-type Document {
-  id: Int!
-  createdAt: Date
-  updatedAt: Date
-  path: String!
-  title: String!
-  subtitle: String
-  parentPath: String
-  parentTitle: String
-  isDirectory: Boolean!
-  isEntry: Boolean!
-  searchContent: String
-  comments: [Comment]
-  tags: [Tag]
-}
-
-type File {
-  id: Int!
-  createdAt: Date
-  updatedAt: Date
-  category: FileType!
-  mime: String!
-  extra: String
-  filename: String!
-  basename: String!
-  filesize: Int!
-  folder: Folder
-}
-
-type Folder {
-  id: Int!
-  createdAt: Date
-  updatedAt: Date
-  name: String!
-  files: [File]
-}
-
-type Right {
-  id: Int!
-  createdAt: Date
-  updatedAt: Date
-  path: String!
-  role: RightRole!
-  exact: Boolean!
-  allow: Boolean!
-  group: Group!
-}
-
-type Setting {
-  id: Int!
-  createdAt: Date
-  updatedAt: Date
-  key: String!
-  config: String!
-}
-
-# Tags are attached to one or more documents
-type Tag {
-  id: Int!
-  createdAt: Date
-  updatedAt: Date
-  key: String!
-  documents: [Document]
-}
-
-type Translation {
-  key: String!
-  value: String!
-}
-
-type OperationResult {
-  succeeded: Boolean!
-  message: String
-  data: String
-}
+# ROOT
+# ----
 
 # Query (Read)
-type Query {
-  comments(id: Int): [Comment]
-  documents(id: Int, path: String): [Document]
-  files(id: Int): [File]
-  folders(id: Int, name: String): [Folder]
-  rights(id: Int): [Right]
-  settings(key: String): [Setting]
-  tags(key: String): [Tag]
-  translations(locale: String!, namespace: String!): [Translation]
-}
+type Query
 
 # Mutations (Create, Update, Delete)
-type Mutation {
-  addRightToGroup(
-    groupId: Int!
-    path: String!
-    role: RightRole!
-    exact: Boolean!
-    allow: Boolean!
-  ): Right
-
-  assignTagToDocument(
-    tagId: Int!
-    documentId: Int!
-  ): OperationResult
-
-  createComment(
-    userId: Int!
-    documentId: Int!
-    content: String!
-  ): Comment
-
-  createDocument(
-    path: String!
-    title: String!
-    subtitle: String
-  ): Document
-
-  createFolder(
-    name: String!
-  ): Folder
-
-  createTag(
-    name: String!
-  ): Tag
-
-  deleteComment(
-    id: Int!
-  ): OperationResult
-
-  deleteDocument(
-    id: Int!
-  ): OperationResult
-
-  deleteFile(
-    id: Int!
-  ): OperationResult
-
-  deleteFolder(
-    id: Int!
-  ): OperationResult
-
-  deleteTag(
-    id: Int!
-  ): OperationResult
-
-  modifyComment(
-    id: Int!
-    content: String!
-  ): Document
-
-  modifyDocument(
-    id: Int!
-    title: String
-    subtitle: String
-  ): Document
-
-  modifyRight(
-    id: Int!
-    path: String
-    role: RightRole
-    exact: Boolean
-    allow: Boolean
-  ): Right
-
-  moveDocument(
-    id: Int!
-    path: String!
-  ): OperationResult
-
-  moveFile(
-    id: Int!
-    folderId: Int!
-  ): OperationResult
-
-  renameFile(
-    id: Int!
-    name: String!
-  ): OperationResult
-
-  renameFolder(
-    id: Int!
-    name: String!
-  ): OperationResult
-
-  renameTag(
-    id: Int!
-    key: String!
-  ): OperationResult
-
-  removeTagFromDocument(
-    tagId: Int!
-    documentId: Int!
-  ): OperationResult
-
-  removeRightFromGroup(
-    rightId: Int!
-  ): OperationResult
-
-  setConfigEntry(
-    key: String!
-    value: String!
-  ): OperationResult
-
-  uploadFile(
-    category: FileType!
-    filename: String!
-  ): File
-}
+type Mutation
 
+# Subscriptions (Push, Real-time)
 type Subscription

+ 2 - 15
server/graph/schemas/group.graphql

@@ -89,7 +89,7 @@ type PageRule {
   id: String!
   deny: Boolean!
   match: PageRuleMatch!
-  roles: [PageRuleRole]!
+  roles: [String]!
   path: String!
   locales: [String]!
 }
@@ -98,24 +98,11 @@ input PageRuleInput {
   id: String!
   deny: Boolean!
   match: PageRuleMatch!
-  roles: [PageRuleRole]!
+  roles: [String]!
   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

+ 6 - 0
server/graph/schemas/localization.graphql

@@ -17,6 +17,7 @@ extend type Mutation {
 type LocalizationQuery {
   locales: [LocalizationLocale]
   config: LocalizationConfig
+  translations(locale: String!, namespace: String!): [Translation]
 }
 
 # -----------------------------------------------
@@ -57,3 +58,8 @@ type LocalizationConfig {
   namespacing: Boolean!
   namespaces: [String]!
 }
+
+type Translation {
+  key: String!
+  value: String!
+}

+ 1 - 26
server/graph/schemas/page.graphql

@@ -19,19 +19,7 @@ type PageQuery {
     id: Int!
     offsetPage: Int
     offsetSize: Int
-  ): PageHistoryResult
-
-  list(
-    filter: String
-    orderBy: String
-  ): [PageMinimal]
-
-  single(
-    id: Int
-    path: String
-    locale: String
-    isPrivate: Boolean
-  ): Page
+  ): PageHistoryResult @auth(requires: ["manage:system", "read:pages"])
 }
 
 # -----------------------------------------------
@@ -82,21 +70,8 @@ type PageResponse {
   page: Page
 }
 
-type PageMinimal {
-  id: Int!
-  name: String!
-  userCount: Int
-  createdAt: Date!
-  updatedAt: Date!
-}
-
 type Page {
   id: Int!
-  name: String!
-  rights: [Right]
-  users: [User]
-  createdAt: Date!
-  updatedAt: Date!
 }
 
 type PageHistory {

+ 1 - 1
server/graph/schemas/site.graphql

@@ -49,7 +49,7 @@ type SiteConfig {
   description: String!
   robots: [String]!
   analyticsService: String!
-    analyticsId: String!
+  analyticsId: String!
   company: String!
   hasLogo: Boolean!
   logoIsSquare: Boolean!

+ 22 - 30
server/graph/schemas/system.graphql

@@ -6,50 +6,42 @@ extend type Query {
   system: SystemQuery
 }
 
-extend type Mutation {
-  system: SystemMutation
-}
-
 # -----------------------------------------------
 # QUERIES
 # -----------------------------------------------
 
 type SystemQuery {
-  info: SystemInfo @auth(requires: ["manage:system"])
+  info: SystemInfo
 }
 
 # -----------------------------------------------
 # MUTATIONS
 # -----------------------------------------------
 
-type SystemMutation {
-  todo: String
-}
-
 # -----------------------------------------------
 # TYPES
 # -----------------------------------------------
 
 type SystemInfo {
-  configFile: String
-  cpuCores: Int
-  currentVersion: String
-  dbHost: String
-  dbType: String
-  dbVersion: String
-  groupsTotal: Int
-  hostname: String
-  latestVersion: String
-  latestVersionReleaseDate: Date
-  nodeVersion: String
-  operatingSystem: String
-  pagesTotal: Int
-  platform: String
-  ramTotal: String
-  redisHost: String
-  redisTotalRAM: String
-  redisUsedRAM: String
-  redisVersion: String
-  usersTotal: Int
-  workingDirectory: String
+  configFile: String @auth(requires: ["manage:system"])
+  cpuCores: Int @auth(requires: ["manage:system"])
+  currentVersion: String @auth(requires: ["manage:system"])
+  dbHost: String @auth(requires: ["manage:system"])
+  dbType: String @auth(requires: ["manage:system"])
+  dbVersion: String @auth(requires: ["manage:system"])
+  groupsTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:groups", "write:groups", "manage:users", "write:users"])
+  hostname: String @auth(requires: ["manage:system"])
+  latestVersion: String @auth(requires: ["manage:system"])
+  latestVersionReleaseDate: Date @auth(requires: ["manage:system"])
+  nodeVersion: String @auth(requires: ["manage:system"])
+  operatingSystem: String @auth(requires: ["manage:system"])
+  pagesTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:pages", "delete:pages"])
+  platform: String @auth(requires: ["manage:system"])
+  ramTotal: String @auth(requires: ["manage:system"])
+  redisHost: String @auth(requires: ["manage:system"])
+  redisTotalRAM: String @auth(requires: ["manage:system"])
+  redisUsedRAM: String @auth(requires: ["manage:system"])
+  redisVersion: String @auth(requires: ["manage:system"])
+  usersTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:groups", "write:groups", "manage:users", "write:users"])
+  workingDirectory: String @auth(requires: ["manage:system"])
 }

+ 29 - 7
server/models/users.js

@@ -101,6 +101,10 @@ module.exports = class User extends Model {
     await this.generateHash()
   }
 
+  // ------------------------------------------------
+  // Instance Methods
+  // ------------------------------------------------
+
   async generateHash() {
     if (this.password) {
       if (bcryptRegexp.test(this.password)) { return }
@@ -138,11 +142,18 @@ module.exports = class User extends Model {
     return (result && _.has(result, 'delta') && result.delta === 0)
   }
 
-  async getPermissions() {
-    const permissions = await this.$relatedQuery('groups').select('permissions').pluck('permissions')
-    this.permissions = _.uniq(_.flatten(permissions))
+  getGlobalPermissions() {
+    return _.uniq(_.flatten(_.map(this.groups, 'permissions')))
+  }
+
+  getGroups() {
+    return _.uniq(_.map(this.groups, 'id'))
   }
 
+  // ------------------------------------------------
+  // Model Methods
+  // ------------------------------------------------
+
   static async processProfile(profile) {
     let primaryEmail = ''
     if (_.isArray(profile.emails)) {
@@ -246,12 +257,17 @@ module.exports = class User extends Model {
 
   static async refreshToken(user) {
     if (_.isSafeInteger(user)) {
-      user = await WIKI.models.users.query().findById(user)
+      user = await WIKI.models.users.query().findById(user).eager('groups').modifyEager('groups', builder => {
+        builder.select('groups.id', 'permissions')
+      })
       if (!user) {
         WIKI.logger.warn(`Failed to refresh token for user ${user}: Not found.`)
         throw new WIKI.Error.AuthGenericError()
       }
+    } else if(_.isNil(user.groups)) {
+      await user.$relatedQuery('groups').select('groups.id', 'permissions')
     }
+
     return {
       token: jwt.sign({
         id: user.id,
@@ -261,7 +277,8 @@ module.exports = class User extends Model {
         timezone: user.timezone,
         localeCode: user.localeCode,
         defaultEditor: user.defaultEditor,
-        permissions: ['manage:system']
+        permissions: user.getGlobalPermissions(),
+        groups: user.getGroups()
       }, {
         key: WIKI.config.certs.private,
         passphrase: WIKI.config.sessionSecret
@@ -398,8 +415,13 @@ module.exports = class User extends Model {
   }
 
   static async getGuestUser () {
-    let user = await WIKI.models.users.query().findById(2)
-    user.getPermissions()
+    const user = await WIKI.models.users.query().findById(2).eager('groups').modifyEager('groups', builder => {
+      builder.select('groups.id', 'permissions')
+    })
+    if (!user) {
+      WIKI.logger.error('CRITICAL ERROR: Guest user is missing!')
+      process.exit(1)
+    }
     return user
   }
 }

+ 2 - 2
server/setup.js

@@ -219,9 +219,9 @@ module.exports = () => {
       })
       const guestGroup = await WIKI.models.groups.query().insert({
         name: 'Guests',
-        permissions: JSON.stringify(['read:pages']),
+        permissions: JSON.stringify(['read:pages', 'read:assets', 'read:comments']),
         pageRules: JSON.stringify([
-          { id: 'guest', roles: ['READ', 'AS_READ', 'CM_READ'], match: 'START', deny: false, path: '', locales: [] }
+          { id: 'guest', roles: ['read:pages', 'read:assets', 'read:comments'], match: 'START', deny: false, path: '', locales: [] }
         ]),
         isSystem: true
       })

+ 13 - 0
server/views/notfound.pug

@@ -0,0 +1,13 @@
+extends master.pug
+
+block body
+  #root.is-fullscreen
+    v-app
+      .notfound
+        .notfound-content
+          img.animated.fadeIn(src='/svg/icon-delete-file.svg', alt='Not Found')
+          .headline= t('notfound.title')
+          .subheading.mt-3= t('notfound.subtitle')
+          v-btn.mt-5(color='red lighten-4', href='/', large, outline)
+            v-icon(left) home
+            span= t('notfound.gohome')