Browse Source

refactor: update schema with new structure / naming

NGPixel 3 years ago
parent
commit
358ad1fdcd
48 changed files with 1166 additions and 2142 deletions
  1. 0 56
      server/graph/directives/auth.js
  2. 0 5
      server/graph/directives/rate-limit.js
  3. 14 3
      server/graph/index.js
  4. 3 9
      server/graph/resolvers/analytics.js
  5. 4 10
      server/graph/resolvers/asset.js
  6. 29 35
      server/graph/resolvers/authentication.js
  7. 8 14
      server/graph/resolvers/comment.js
  8. 0 28
      server/graph/resolvers/contribute.js
  9. 0 32
      server/graph/resolvers/folder.js
  10. 8 14
      server/graph/resolvers/group.js
  11. 1 15
      server/graph/resolvers/localization.js
  12. 4 10
      server/graph/resolvers/mail.js
  13. 5 11
      server/graph/resolvers/navigation.js
  14. 19 25
      server/graph/resolvers/page.js
  15. 1 7
      server/graph/resolvers/rendering.js
  16. 1 65
      server/graph/resolvers/search.js
  17. 1 115
      server/graph/resolvers/site.js
  18. 198 68
      server/graph/resolvers/storage.js
  19. 28 255
      server/graph/resolvers/system.js
  20. 0 58
      server/graph/resolvers/tag.js
  21. 0 62
      server/graph/resolvers/theming.js
  22. 66 44
      server/graph/resolvers/user.js
  23. 15 18
      server/graph/scalars/date.js
  24. 15 17
      server/graph/scalars/json.js
  25. 23 26
      server/graph/scalars/uuid.js
  26. 5 27
      server/graph/schemas/analytics.graphql
  27. 10 26
      server/graph/schemas/asset.graphql
  28. 15 33
      server/graph/schemas/authentication.graphql
  29. 28 44
      server/graph/schemas/comment.graphql
  30. 8 12
      server/graph/schemas/common.graphql
  31. 0 29
      server/graph/schemas/contribute.graphql
  32. 36 57
      server/graph/schemas/group.graphql
  33. 13 37
      server/graph/schemas/localization.graphql
  34. 0 64
      server/graph/schemas/logging.graphql
  35. 17 33
      server/graph/schemas/mail.graphql
  36. 12 24
      server/graph/schemas/navigation.graphql
  37. 133 149
      server/graph/schemas/page.graphql
  38. 7 23
      server/graph/schemas/rendering.graphql
  39. 1 45
      server/graph/schemas/search.graphql
  40. 1 94
      server/graph/schemas/site.graphql
  41. 51 50
      server/graph/schemas/storage.graphql
  42. 97 77
      server/graph/schemas/system.graphql
  43. 0 54
      server/graph/schemas/theming.graphql
  44. 101 114
      server/graph/schemas/user.graphql
  45. 102 81
      ux/src/components/SiteActivateDialog.vue
  46. 5 3
      ux/src/components/SiteCreateDialog.vue
  47. 81 60
      ux/src/components/SiteDeleteDialog.vue
  48. 0 4
      ux/src/pages/AdminSites.vue

+ 0 - 56
server/graph/directives/auth.js

@@ -1,56 +0,0 @@
-const { SchemaDirectiveVisitor } = require('graphql-tools')
-const { defaultFieldResolver } = require('graphql')
-const _ = require('lodash')
-
-class AuthDirective extends SchemaDirectiveVisitor {
-  visitObject(type) {
-    this.ensureFieldsWrapped(type)
-    type._requiredAuthScopes = this.args.requires
-  }
-  // Visitor methods for nested types like fields and arguments
-  // also receive a details object that provides information about
-  // the parent and grandparent types.
-  visitFieldDefinition(field, details) {
-    this.ensureFieldsWrapped(details.objectType)
-    field._requiredAuthScopes = this.args.requires
-  }
-
-  visitArgumentDefinition(argument, details) {
-    this.ensureFieldsWrapped(details.objectType)
-    argument._requiredAuthScopes = this.args.requires
-  }
-
-  ensureFieldsWrapped(objectType) {
-    // Mark the GraphQLObjectType object to avoid re-wrapping:
-    if (objectType._authFieldsWrapped) return
-    objectType._authFieldsWrapped = true
-
-    const fields = objectType.getFields()
-
-    Object.keys(fields).forEach(fieldName => {
-      const field = fields[fieldName]
-      const { resolve = defaultFieldResolver } = field
-      field.resolve = async function (...args) {
-        // Get the required scopes from the field first, falling back
-        // to the objectType if no scopes is required by the field:
-        const requiredScopes = field._requiredAuthScopes || objectType._requiredAuthScopes
-
-        if (!requiredScopes) {
-          return resolve.apply(this, args)
-        }
-
-        const context = args[2]
-        if (!context.req.user) {
-          throw new Error('Unauthorized')
-        }
-        if (!_.some(context.req.user.permissions, pm => _.includes(requiredScopes, pm))) {
-          throw new Error('Forbidden')
-        }
-
-        return resolve.apply(this, args)
-      }
-    })
-  }
-}
-
-module.exports = AuthDirective

+ 0 - 5
server/graph/directives/rate-limit.js

@@ -1,5 +0,0 @@
-const { createRateLimitDirective } = require('graphql-rate-limit-directive')
-
-module.exports = createRateLimitDirective({
-  keyGenerator: (directiveArgs, source, args, context, info) => `${context.req.ip}:${info.parentType}.${info.fieldName}`
-})

+ 14 - 3
server/graph/index.js

@@ -3,16 +3,20 @@ const fs = require('fs')
 const path = require('path')
 const autoload = require('auto-load')
 const { makeExecutableSchema } = require('@graphql-tools/schema')
-const { rateLimitDirective } = require('graphql-rate-limit-directive')
+const { defaultKeyGenerator, rateLimitDirective } = require('graphql-rate-limit-directive')
 const { GraphQLUpload } = require('graphql-upload')
-const { rateLimitDirectiveTypeDefs, rateLimitDirectiveTransformer } = rateLimitDirective()
 
 /* global WIKI */
 
-WIKI.logger.info(`Loading GraphQL Schema...`)
+// Rate Limiter
+
+const { rateLimitDirectiveTypeDefs, rateLimitDirectiveTransformer } = rateLimitDirective({
+  keyGenerator: (directiveArgs, source, args, context, info) => `${context.req.ip}:${defaultKeyGenerator(directiveArgs, source, args, context, info)}`
+})
 
 // Schemas
 
+WIKI.logger.info(`Loading GraphQL Schema...`)
 const typeDefs = [
   rateLimitDirectiveTypeDefs
 ]
@@ -23,7 +27,11 @@ schemas.forEach(schema => {
 
 // Resolvers
 
+WIKI.logger.info(`Loading GraphQL Resolvers...`)
 let resolvers = {
+  Date: require('./scalars/date'),
+  JSON: require('./scalars/json'),
+  UUID: require('./scalars/uuid'),
   Upload: GraphQLUpload
 }
 const resolversObj = _.values(autoload(path.join(WIKI.SERVERPATH, 'graph/resolvers')))
@@ -33,11 +41,14 @@ resolversObj.forEach(resolver => {
 
 // Make executable schema
 
+WIKI.logger.info(`Compiling GraphQL Schema...`)
 let schema = makeExecutableSchema({
   typeDefs,
   resolvers
 })
 
+// Apply schema transforms
+
 schema = rateLimitDirectiveTransformer(schema)
 
 WIKI.logger.info(`GraphQL Schema: [ OK ]`)

+ 3 - 9
server/graph/resolvers/analytics.js

@@ -5,13 +5,7 @@ const graphHelper = require('../../helpers/graph')
 
 module.exports = {
   Query: {
-    async analytics() { return {} }
-  },
-  Mutation: {
-    async analytics() { return {} }
-  },
-  AnalyticsQuery: {
-    async providers(obj, args, context, info) {
+    async analyticsProviders(obj, args, context, info) {
       let providers = await WIKI.models.analytics.getProviders(args.isEnabled)
       providers = providers.map(stg => {
         const providerInfo = _.find(WIKI.data.analytics, ['key', stg.key]) || {}
@@ -33,8 +27,8 @@ module.exports = {
       return providers
     }
   },
-  AnalyticsMutation: {
-    async updateProviders(obj, args, context) {
+  Mutation: {
+    async updateAnalyticsProviders(obj, args, context) {
       try {
         for (let str of args.providers) {
           await WIKI.models.analytics.query().patch({

+ 4 - 10
server/graph/resolvers/asset.js

@@ -7,13 +7,7 @@ const assetHelper = require('../../helpers/asset')
 
 module.exports = {
   Query: {
-    async assets() { return {} }
-  },
-  Mutation: {
-    async assets() { return {} }
-  },
-  AssetQuery: {
-    async list(obj, args, context) {
+    async assets(obj, args, context) {
       let cond = {
         folderId: args.folderId === 0 ? null : args.folderId
       }
@@ -31,7 +25,7 @@ module.exports = {
         kind: a.kind.toUpperCase()
       }))
     },
-    async folders(obj, args, context) {
+    async assetsFolders(obj, args, context) {
       const results = await WIKI.models.assetFolders.query().where({
         parentId: args.parentFolderId === 0 ? null : args.parentFolderId
       })
@@ -43,11 +37,11 @@ module.exports = {
       })
     }
   },
-  AssetMutation: {
+  Mutation: {
     /**
      * Create New Asset Folder
      */
-    async createFolder(obj, args, context) {
+    async createAssetsFolder(obj, args, context) {
       try {
         const folderSlug = sanitize(args.slug).toLowerCase()
         const parentFolderId = args.parentFolderId === 0 ? null : args.parentFolderId

+ 29 - 35
server/graph/resolvers/authentication.js

@@ -7,12 +7,6 @@ const graphHelper = require('../../helpers/graph')
 
 module.exports = {
   Query: {
-    async authentication () { return {} }
-  },
-  Mutation: {
-    async authentication () { return {} }
-  },
-  AuthenticationQuery: {
     /**
      * List of API Keys
      */
@@ -34,7 +28,7 @@ module.exports = {
     apiState () {
       return WIKI.config.api.isEnabled
     },
-    async strategies () {
+    async authStrategies () {
       return WIKI.data.authentication.map(stg => ({
         ...stg,
         isAvailable: stg.isAvailable === true,
@@ -45,35 +39,35 @@ module.exports = {
           })
         }, []), 'key')
       }))
-    },
-    /**
-     * Fetch active authentication strategies
-     */
-    async activeStrategies (obj, args, context, info) {
-      let strategies = await WIKI.models.authentication.getStrategies()
-      strategies = strategies.map(stg => {
-        const strategyInfo = _.find(WIKI.data.authentication, ['key', stg.strategyKey]) || {}
-        return {
-          ...stg,
-          strategy: strategyInfo,
-          config: _.sortBy(_.transform(stg.config, (res, value, key) => {
-            const configData = _.get(strategyInfo.props, key, false)
-            if (configData) {
-              res.push({
-                key,
-                value: JSON.stringify({
-                  ...configData,
-                  value
-                })
-              })
-            }
-          }, []), 'key')
-        }
-      })
-      return args.enabledOnly ? _.filter(strategies, 'isEnabled') : strategies
     }
+    // /**
+    //  * Fetch active authentication strategies
+    //  */
+    // async activeStrategies (obj, args, context, info) {
+    //   let strategies = await WIKI.models.authentication.getStrategies()
+    //   strategies = strategies.map(stg => {
+    //     const strategyInfo = _.find(WIKI.data.authentication, ['key', stg.strategyKey]) || {}
+    //     return {
+    //       ...stg,
+    //       strategy: strategyInfo,
+    //       config: _.sortBy(_.transform(stg.config, (res, value, key) => {
+    //         const configData = _.get(strategyInfo.props, key, false)
+    //         if (configData) {
+    //           res.push({
+    //             key,
+    //             value: JSON.stringify({
+    //               ...configData,
+    //               value
+    //             })
+    //           })
+    //         }
+    //       }, []), 'key')
+    //     }
+    //   })
+    //   return args.enabledOnly ? _.filter(strategies, 'isEnabled') : strategies
+    // }
   },
-  AuthenticationMutation: {
+  Mutation: {
     /**
      * Create New API Key
      */
@@ -197,7 +191,7 @@ module.exports = {
     /**
      * Update Authentication Strategies
      */
-    async updateStrategies (obj, args, context) {
+    async updateAuthStrategies (obj, args, context) {
       try {
         const previousStrategies = await WIKI.models.authentication.getStrategies()
         for (const str of args.strategies) {

+ 8 - 14
server/graph/resolvers/comment.js

@@ -5,16 +5,10 @@ const graphHelper = require('../../helpers/graph')
 
 module.exports = {
   Query: {
-    async comments() { return {} }
-  },
-  Mutation: {
-    async comments() { return {} }
-  },
-  CommentQuery: {
     /**
      * Fetch list of Comments Providers
      */
-    async providers(obj, args, context, info) {
+    async commentsProviders(obj, args, context, info) {
       const providers = await WIKI.models.commentProviders.getProviders()
       return providers.map(provider => {
         const providerInfo = _.find(WIKI.data.commentProviders, ['key', provider.key]) || {}
@@ -39,7 +33,7 @@ module.exports = {
     /**
      * Fetch list of comments for a page
      */
-    async list (obj, args, context) {
+    async comments (obj, args, context) {
       const page = await WIKI.models.pages.query().select('id').findOne({ localeCode: args.locale, path: args.path })
       if (page) {
         if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], args)) {
@@ -60,7 +54,7 @@ module.exports = {
     /**
      * Fetch a single comment
      */
-    async single (obj, args, context) {
+    async commentById (obj, args, context) {
       const cm = await WIKI.data.commentProvider.getCommentById(args.id)
       if (!cm || !cm.pageId) {
         throw new WIKI.Error.CommentNotFound()
@@ -86,11 +80,11 @@ module.exports = {
       }
     }
   },
-  CommentMutation: {
+  Mutation: {
     /**
      * Create New Comment
      */
-    async create (obj, args, context) {
+    async createComment (obj, args, context) {
       try {
         const cmId = await WIKI.models.comments.postNewComment({
           ...args,
@@ -108,7 +102,7 @@ module.exports = {
     /**
      * Update an Existing Comment
      */
-    async update (obj, args, context) {
+    async updateComment (obj, args, context) {
       try {
         const cmRender = await WIKI.models.comments.updateComment({
           ...args,
@@ -126,7 +120,7 @@ module.exports = {
     /**
      * Delete an Existing Comment
      */
-    async delete (obj, args, context) {
+    async deleteComment (obj, args, context) {
       try {
         await WIKI.models.comments.deleteComment({
           id: args.id,
@@ -143,7 +137,7 @@ module.exports = {
     /**
      * Update Comments Providers
      */
-    async updateProviders(obj, args, context) {
+    async updateCommentsProviders(obj, args, context) {
       try {
         for (let provider of args.providers) {
           await WIKI.models.commentProviders.query().patch({

+ 0 - 28
server/graph/resolvers/contribute.js

@@ -1,28 +0,0 @@
-const request = require('request-promise')
-const _ = require('lodash')
-
-/* global WIKI */
-
-module.exports = {
-  Query: {
-    async contribute() { return {} }
-  },
-  ContributeQuery: {
-    async contributors(obj, args, context, info) {
-      try {
-        const resp = await request({
-          method: 'POST',
-          uri: 'https://graph.requarks.io',
-          json: true,
-          body: {
-            query: '{\n  sponsors {\n    list(kind: BACKER) {\n      id\n      source\n      name\n      joined\n      website\n      twitter\n      avatar\n    }\n  }\n}\n',
-            variables: {}
-          }
-        })
-        return _.get(resp, 'data.sponsors.list', [])
-      } catch (err) {
-        WIKI.logger.warn(err)
-      }
-    }
-  }
-}

+ 0 - 32
server/graph/resolvers/folder.js

@@ -1,32 +0,0 @@
-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()
-  //   }
-  // }
-}

+ 8 - 14
server/graph/resolvers/group.js

@@ -7,16 +7,10 @@ const gql = require('graphql')
 
 module.exports = {
   Query: {
-    async groups () { return {} }
-  },
-  Mutation: {
-    async groups () { return {} }
-  },
-  GroupQuery: {
     /**
      * FETCH ALL GROUPS
      */
-    async list () {
+    async groups () {
       return WIKI.models.groups.query().select(
         'groups.*',
         WIKI.models.groups.relatedQuery('users').count().as('userCount')
@@ -25,15 +19,15 @@ module.exports = {
     /**
      * FETCH A SINGLE GROUP
      */
-    async single(obj, args) {
+    async groupById(obj, args) {
       return WIKI.models.groups.query().findById(args.id)
     }
   },
-  GroupMutation: {
+  Mutation: {
     /**
      * ASSIGN USER TO GROUP
      */
-    async assignUser (obj, args, { req }) {
+    async assignUserToGroup (obj, args, { req }) {
       // Check for guest user
       if (args.userId === 2) {
         throw new gql.GraphQLError('Cannot assign the Guest user to a group.')
@@ -85,7 +79,7 @@ module.exports = {
     /**
      * CREATE NEW GROUP
      */
-    async create (obj, args, { req }) {
+    async createGroup (obj, args, { req }) {
       const group = await WIKI.models.groups.query().insertAndFetch({
         name: args.name,
         permissions: JSON.stringify(WIKI.data.groups.defaultPermissions),
@@ -102,7 +96,7 @@ module.exports = {
     /**
      * DELETE GROUP
      */
-    async delete (obj, args) {
+    async deleteGroup (obj, args) {
       if (args.id === 1 || args.id === 2) {
         throw new gql.GraphQLError('Cannot delete this group.')
       }
@@ -122,7 +116,7 @@ module.exports = {
     /**
      * UNASSIGN USER FROM GROUP
      */
-    async unassignUser (obj, args) {
+    async unassignUserFromGroup (obj, args) {
       if (args.userId === 2) {
         throw new gql.GraphQLError('Cannot unassign Guest user')
       }
@@ -149,7 +143,7 @@ module.exports = {
     /**
      * UPDATE GROUP
      */
-    async update (obj, args, { req }) {
+    async updateGroup (obj, args, { req }) {
       // Check for unsafe regex page rules
       if (_.some(args.pageRules, pr => {
         return pr.match === 'REGEX' && !safeRegex(pr.path)

+ 1 - 15
server/graph/resolvers/localization.js

@@ -5,12 +5,6 @@ const _ = require('lodash')
 
 module.exports = {
   Query: {
-    async localization() { return {} }
-  },
-  Mutation: {
-    async localization() { return {} }
-  },
-  LocalizationQuery: {
     async locales(obj, args, context, info) {
       let remoteLocales = await WIKI.cache.get('locales')
       let localLocales = await WIKI.models.locales.query().select('code', 'isRTL', 'name', 'nativeName', 'createdAt', 'updatedAt', 'availability')
@@ -24,19 +18,11 @@ module.exports = {
         }
       })
     },
-    async config(obj, args, context, info) {
-      return {
-        locale: WIKI.config.lang.code,
-        autoUpdate: WIKI.config.lang.autoUpdate,
-        namespacing: WIKI.config.lang.namespacing,
-        namespaces: WIKI.config.lang.namespaces
-      }
-    },
     translations (obj, args, context, info) {
       return WIKI.lang.getByNamespace(args.locale, args.namespace)
     }
   },
-  LocalizationMutation: {
+  Mutation: {
     async downloadLocale(obj, args, context) {
       try {
         const job = await WIKI.scheduler.registerJob({

+ 4 - 10
server/graph/resolvers/mail.js

@@ -5,21 +5,15 @@ const graphHelper = require('../../helpers/graph')
 
 module.exports = {
   Query: {
-    async mail() { return {} }
-  },
-  Mutation: {
-    async mail() { return {} }
-  },
-  MailQuery: {
-    async config(obj, args, context, info) {
+    async mailConfig(obj, args, context, info) {
       return {
         ...WIKI.config.mail,
         pass: WIKI.config.mail.pass.length > 0 ? '********' : ''
       }
     }
   },
-  MailMutation: {
-    async sendTest(obj, args, context) {
+  Mutation: {
+    async sendMailTest(obj, args, context) {
       try {
         if (_.isEmpty(args.recipientEmail) || args.recipientEmail.length < 6) {
           throw new WIKI.Error.MailInvalidRecipient()
@@ -42,7 +36,7 @@ module.exports = {
         return graphHelper.generateError(err)
       }
     },
-    async updateConfig(obj, args, context) {
+    async updateMailConfig(obj, args, context) {
       try {
         WIKI.config.mail = {
           senderName: args.senderName,

+ 5 - 11
server/graph/resolvers/navigation.js

@@ -4,21 +4,15 @@ const graphHelper = require('../../helpers/graph')
 
 module.exports = {
   Query: {
-    async navigation () { return {} }
-  },
-  Mutation: {
-    async navigation () { return {} }
-  },
-  NavigationQuery: {
-    async tree (obj, args, context, info) {
+    async navigationTree (obj, args, context, info) {
       return WIKI.models.navigation.getTree({ cache: false, locale: 'all', bypassAuth: true })
     },
-    config (obj, args, context, info) {
+    navigationConfig (obj, args, context, info) {
       return WIKI.config.nav
     }
   },
-  NavigationMutation: {
-    async updateTree (obj, args, context) {
+  Mutation: {
+    async updateNavigationTree (obj, args, context) {
       try {
         await WIKI.models.navigation.query().patch({
           config: args.tree
@@ -34,7 +28,7 @@ module.exports = {
         return graphHelper.generateError(err)
       }
     },
-    async updateConfig (obj, args, context) {
+    async updateNavigationConfig (obj, args, context) {
       try {
         WIKI.config.nav = {
           mode: args.mode

+ 19 - 25
server/graph/resolvers/page.js

@@ -5,16 +5,10 @@ const graphHelper = require('../../helpers/graph')
 
 module.exports = {
   Query: {
-    async pages() { return {} }
-  },
-  Mutation: {
-    async pages() { return {} }
-  },
-  PageQuery: {
     /**
      * PAGE HISTORY
      */
-    async history(obj, args, context, info) {
+    async pageHistoryById (obj, args, context, info) {
       const page = await WIKI.models.pages.query().select('path', 'localeCode').findById(args.id)
       if (WIKI.auth.checkAccess(context.req.user, ['read:history'], {
         path: page.path,
@@ -32,7 +26,7 @@ module.exports = {
     /**
      * PAGE VERSION
      */
-    async version(obj, args, context, info) {
+    async pageVersionById (obj, args, context, info) {
       const page = await WIKI.models.pages.query().select('path', 'localeCode').findById(args.pageId)
       if (WIKI.auth.checkAccess(context.req.user, ['read:history'], {
         path: page.path,
@@ -49,7 +43,7 @@ module.exports = {
     /**
      * SEARCH PAGES
      */
-    async search (obj, args, context) {
+    async searchPages (obj, args, context) {
       if (WIKI.data.searchEngine) {
         const resp = await WIKI.data.searchEngine.query(args.query, args)
         return {
@@ -73,7 +67,7 @@ module.exports = {
     /**
      * LIST PAGES
      */
-    async list (obj, args, context, info) {
+    async pages (obj, args, context, info) {
       let results = await WIKI.models.pages.query().column([
         'pages.id',
         'path',
@@ -149,7 +143,7 @@ module.exports = {
     /**
      * FETCH SINGLE PAGE
      */
-    async single (obj, args, context, info) {
+    async pageById (obj, args, context, info) {
       let page = await WIKI.models.pages.getPageFromDb(args.id)
       if (page) {
         if (WIKI.auth.checkAccess(context.req.user, ['manage:pages', 'delete:pages'], {
@@ -220,7 +214,7 @@ module.exports = {
     /**
      * FETCH PAGE TREE
      */
-    async tree (obj, args, context, info) {
+    async pageTree (obj, args, context, info) {
       let curPage = null
 
       if (!args.locale) { args.locale = WIKI.config.lang.code }
@@ -270,7 +264,7 @@ module.exports = {
     /**
      * FETCH PAGE LINKS
      */
-    async links (obj, args, context, info) {
+    async pageLinks (obj, args, context, info) {
       let results
 
       if (WIKI.config.db.type === 'mysql' || WIKI.config.db.type === 'mariadb' || WIKI.config.db.type === 'sqlite') {
@@ -343,7 +337,7 @@ module.exports = {
     /**
      * FETCH LATEST VERSION FOR CONFLICT COMPARISON
      */
-    async conflictLatest (obj, args, context, info) {
+    async checkConflictsLatest (obj, args, context, info) {
       let page = await WIKI.models.pages.getPageFromDb(args.id)
       if (page) {
         if (WIKI.auth.checkAccess(context.req.user, ['write:pages', 'manage:pages'], {
@@ -363,11 +357,11 @@ module.exports = {
       }
     }
   },
-  PageMutation: {
+  Mutation: {
     /**
      * CREATE PAGE
      */
-    async create(obj, args, context) {
+    async createPage(obj, args, context) {
       try {
         const page = await WIKI.models.pages.createPage({
           ...args,
@@ -384,7 +378,7 @@ module.exports = {
     /**
      * UPDATE PAGE
      */
-    async update(obj, args, context) {
+    async updatePage(obj, args, context) {
       try {
         const page = await WIKI.models.pages.updatePage({
           ...args,
@@ -401,7 +395,7 @@ module.exports = {
     /**
      * CONVERT PAGE
      */
-    async convert(obj, args, context) {
+    async convertPage(obj, args, context) {
       try {
         await WIKI.models.pages.convertPage({
           ...args,
@@ -415,9 +409,9 @@ module.exports = {
       }
     },
     /**
-     * MOVE PAGE
+     * RENAME PAGE
      */
-    async move(obj, args, context) {
+    async renamePage(obj, args, context) {
       try {
         await WIKI.models.pages.movePage({
           ...args,
@@ -433,7 +427,7 @@ module.exports = {
     /**
      * DELETE PAGE
      */
-    async delete(obj, args, context) {
+    async deletePage(obj, args, context) {
       try {
         await WIKI.models.pages.deletePage({
           ...args,
@@ -517,7 +511,7 @@ module.exports = {
     /**
      * REBUILD TREE
      */
-    async rebuildTree(obj, args, context) {
+    async rebuildPageTree(obj, args, context) {
       try {
         await WIKI.models.pages.rebuildTree()
         return {
@@ -530,7 +524,7 @@ module.exports = {
     /**
      * RENDER PAGE
      */
-    async render (obj, args, context) {
+    async renderPage (obj, args, context) {
       try {
         const page = await WIKI.models.pages.query().findById(args.id)
         if (!page) {
@@ -547,7 +541,7 @@ module.exports = {
     /**
      * RESTORE PAGE VERSION
      */
-    async restore (obj, args, context) {
+    async restorePage (obj, args, context) {
       try {
         const page = await WIKI.models.pages.query().select('path', 'localeCode').findById(args.pageId)
         if (!page) {
@@ -583,7 +577,7 @@ module.exports = {
     /**
      * Purge history
      */
-    async purgeHistory (obj, args, context) {
+    async purgePagesHistory (obj, args, context) {
       try {
         await WIKI.models.pageHistory.purge(args.olderThan)
         return {

+ 1 - 7
server/graph/resolvers/rendering.js

@@ -5,12 +5,6 @@ const graphHelper = require('../../helpers/graph')
 
 module.exports = {
   Query: {
-    async rendering() { return {} }
-  },
-  Mutation: {
-    async rendering() { return {} }
-  },
-  RenderingQuery: {
     async renderers(obj, args, context, info) {
       let renderers = await WIKI.models.renderers.getRenderers()
       renderers = renderers.map(rdr => {
@@ -37,7 +31,7 @@ module.exports = {
       return renderers
     }
   },
-  RenderingMutation: {
+  Mutation: {
     async updateRenderers(obj, args, context) {
       try {
         for (let rdr of args.renderers) {

+ 1 - 65
server/graph/resolvers/search.js

@@ -1,74 +1,10 @@
-const _ = require('lodash')
 const graphHelper = require('../../helpers/graph')
 
 /* global WIKI */
 
 module.exports = {
-  Query: {
-    async search() { return {} }
-  },
   Mutation: {
-    async search() { return {} }
-  },
-  SearchQuery: {
-    async searchEngines(obj, args, context, info) {
-      let searchEngines = await WIKI.models.searchEngines.getSearchEngines()
-      searchEngines = searchEngines.map(searchEngine => {
-        const searchEngineInfo = _.find(WIKI.data.searchEngines, ['key', searchEngine.key]) || {}
-        return {
-          ...searchEngineInfo,
-          ...searchEngine,
-          config: _.sortBy(_.transform(searchEngine.config, (res, value, key) => {
-            const configData = _.get(searchEngineInfo.props, key, false)
-            if (configData) {
-              res.push({
-                key,
-                value: JSON.stringify({
-                  ...configData,
-                  value
-                })
-              })
-            }
-          }, []), 'key')
-        }
-      })
-      // if (args.filter) { searchEngines = graphHelper.filter(searchEngines, args.filter) }
-      if (args.orderBy) { searchEngines = _.sortBy(searchEngines, [args.orderBy]) }
-      return searchEngines
-    }
-  },
-  SearchMutation: {
-    async updateSearchEngines(obj, args, context) {
-      try {
-        let newActiveEngine = ''
-        for (let searchEngine of args.engines) {
-          if (searchEngine.isEnabled) {
-            newActiveEngine = searchEngine.key
-          }
-          await WIKI.models.searchEngines.query().patch({
-            isEnabled: searchEngine.isEnabled,
-            config: _.reduce(searchEngine.config, (result, value, key) => {
-              _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null))
-              return result
-            }, {})
-          }).where('key', searchEngine.key)
-        }
-        if (newActiveEngine !== WIKI.data.searchEngine.key) {
-          try {
-            await WIKI.data.searchEngine.deactivate()
-          } catch (err) {
-            WIKI.logger.warn('Failed to deactivate previous search engine:', err)
-          }
-        }
-        await WIKI.models.searchEngines.initEngine({ activate: true })
-        return {
-          responseResult: graphHelper.generateSuccess('Search Engines updated successfully')
-        }
-      } catch (err) {
-        return graphHelper.generateError(err)
-      }
-    },
-    async rebuildIndex (obj, args, context) {
+    async rebuildSearchIndex (obj, args, context) {
       try {
         await WIKI.data.searchEngine.rebuild()
         return {

+ 1 - 115
server/graph/resolvers/site.js

@@ -40,9 +40,7 @@ module.exports = {
         hostname: site.hostname,
         isEnabled: site.isEnabled
       } : null
-    },
-    // LEGACY
-    async site() { return {} }
+    }
   },
   Mutation: {
     /**
@@ -178,118 +176,6 @@ module.exports = {
       return {
         status: graphHelper.generateSuccess('Site favicon uploaded successfully')
       }
-    },
-    // LEGACY
-    async site() { return {} }
-  },
-  SiteQuery: {
-    async config(obj, args, context, info) {
-      return {
-        host: WIKI.config.host,
-        title: WIKI.config.title,
-        company: WIKI.config.company,
-        contentLicense: WIKI.config.contentLicense,
-        logoUrl: WIKI.config.logoUrl,
-        ...WIKI.config.seo,
-        ...WIKI.config.features,
-        ...WIKI.config.security,
-        authAutoLogin: WIKI.config.auth.autoLogin,
-        authEnforce2FA: WIKI.config.auth.enforce2FA,
-        authHideLocal: WIKI.config.auth.hideLocal,
-        authLoginBgUrl: WIKI.config.auth.loginBgUrl,
-        authJwtAudience: WIKI.config.auth.audience,
-        authJwtExpiration: WIKI.config.auth.tokenExpiration,
-        authJwtRenewablePeriod: WIKI.config.auth.tokenRenewal,
-        uploadMaxFileSize: WIKI.config.uploads.maxFileSize,
-        uploadMaxFiles: WIKI.config.uploads.maxFiles,
-        uploadScanSVG: WIKI.config.uploads.scanSVG,
-        uploadForceDownload: WIKI.config.uploads.forceDownload
-      }
-    }
-  },
-  SiteMutation: {
-    async updateConfig(obj, args, context) {
-      try {
-        if (args.host) {
-          let siteHost = _.trim(args.host)
-          if (siteHost.endsWith('/')) {
-            siteHost = siteHost.slice(0, -1)
-          }
-          WIKI.config.host = siteHost
-        }
-
-        if (args.title) {
-          WIKI.config.title = _.trim(args.title)
-        }
-
-        if (args.company) {
-          WIKI.config.company = _.trim(args.company)
-        }
-
-        if (args.contentLicense) {
-          WIKI.config.contentLicense = args.contentLicense
-        }
-
-        if (args.logoUrl) {
-          WIKI.config.logoUrl = _.trim(args.logoUrl)
-        }
-
-        WIKI.config.seo = {
-          description: _.get(args, 'description', WIKI.config.seo.description),
-          robots: _.get(args, 'robots', WIKI.config.seo.robots),
-          analyticsService: _.get(args, 'analyticsService', WIKI.config.seo.analyticsService),
-          analyticsId: _.get(args, 'analyticsId', WIKI.config.seo.analyticsId)
-        }
-
-        WIKI.config.auth = {
-          autoLogin: _.get(args, 'authAutoLogin', WIKI.config.auth.autoLogin),
-          enforce2FA: _.get(args, 'authEnforce2FA', WIKI.config.auth.enforce2FA),
-          hideLocal: _.get(args, 'authHideLocal', WIKI.config.auth.hideLocal),
-          loginBgUrl: _.get(args, 'authLoginBgUrl', WIKI.config.auth.loginBgUrl),
-          audience: _.get(args, 'authJwtAudience', WIKI.config.auth.audience),
-          tokenExpiration: _.get(args, 'authJwtExpiration', WIKI.config.auth.tokenExpiration),
-          tokenRenewal: _.get(args, 'authJwtRenewablePeriod', WIKI.config.auth.tokenRenewal)
-        }
-
-        WIKI.config.features = {
-          featurePageRatings: _.get(args, 'featurePageRatings', WIKI.config.features.featurePageRatings),
-          featurePageComments: _.get(args, 'featurePageComments', WIKI.config.features.featurePageComments),
-          featurePersonalWikis: _.get(args, 'featurePersonalWikis', WIKI.config.features.featurePersonalWikis)
-        }
-
-        WIKI.config.security = {
-          securityOpenRedirect: _.get(args, 'securityOpenRedirect', WIKI.config.security.securityOpenRedirect),
-          securityIframe: _.get(args, 'securityIframe', WIKI.config.security.securityIframe),
-          securityReferrerPolicy: _.get(args, 'securityReferrerPolicy', WIKI.config.security.securityReferrerPolicy),
-          securityTrustProxy: _.get(args, 'securityTrustProxy', WIKI.config.security.securityTrustProxy),
-          securitySRI: _.get(args, 'securitySRI', WIKI.config.security.securitySRI),
-          securityHSTS: _.get(args, 'securityHSTS', WIKI.config.security.securityHSTS),
-          securityHSTSDuration: _.get(args, 'securityHSTSDuration', WIKI.config.security.securityHSTSDuration),
-          securityCSP: _.get(args, 'securityCSP', WIKI.config.security.securityCSP),
-          securityCSPDirectives: _.get(args, 'securityCSPDirectives', WIKI.config.security.securityCSPDirectives)
-        }
-
-        WIKI.config.uploads = {
-          maxFileSize: _.get(args, 'uploadMaxFileSize', WIKI.config.uploads.maxFileSize),
-          maxFiles: _.get(args, 'uploadMaxFiles', WIKI.config.uploads.maxFiles),
-          scanSVG: _.get(args, 'uploadScanSVG', WIKI.config.uploads.scanSVG),
-          forceDownload: _.get(args, 'uploadForceDownload', WIKI.config.uploads.forceDownload)
-        }
-
-        await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'seo', 'logoUrl', 'auth', 'features', 'security', 'uploads'])
-
-        if (WIKI.config.security.securityTrustProxy) {
-          WIKI.app.enable('trust proxy')
-        } else {
-          WIKI.app.disable('trust proxy')
-        }
-
-        return {
-          responseResult: graphHelper.generateSuccess('Site configuration updated successfully')
-        }
-      } catch (err) {
-        return graphHelper.generateError(err)
-      }
     }
   }
 }

+ 198 - 68
server/graph/resolvers/storage.js

@@ -1,97 +1,227 @@
 const _ = require('lodash')
 const graphHelper = require('../../helpers/graph')
+const { v4: uuid } = require('uuid')
 
 /* global WIKI */
 
 module.exports = {
   Query: {
-    async storage() { return {} }
-  },
-  Mutation: {
-    async storage() { return {} }
-  },
-  StorageQuery: {
-    async targets(obj, args, context, info) {
-      let targets = await WIKI.models.storage.getTargets()
-      targets = _.sortBy(targets.map(tgt => {
-        const targetInfo = _.find(WIKI.data.storage, ['key', tgt.key]) || {}
+    async storageTargets (obj, args, context, info) {
+      const dbTargets = await WIKI.models.storage.getTargets({ siteId: args.siteId })
+      // targets = _.sortBy(targets.map(tgt => {
+      //   const targetInfo = _.find(WIKI.data.storage, ['module', tgt.key]) || {}
+      //   return {
+      //     ...targetInfo,
+      //     ...tgt,
+      //     hasSchedule: (targetInfo.schedule !== false),
+      //     syncInterval: targetInfo.syncInterval || targetInfo.schedule || 'P0D',
+      //     syncIntervalDefault: targetInfo.schedule,
+      //     config: _.sortBy(_.transform(tgt.config, (res, value, key) => {
+      //       const configData = _.get(targetInfo.props, key, false)
+      //       if (configData) {
+      //         res.push({
+      //           key,
+      //           value: JSON.stringify({
+      //             ...configData,
+      //             value: (configData.sensitive && value.length > 0) ? '********' : value
+      //           })
+      //         })
+      //       }
+      //     }, []), 'key')
+      //   }
+      // }), ['title', 'key'])
+      return _.sortBy(WIKI.storage.defs.map(md => {
+        const dbTarget = dbTargets.find(tg => tg.module === md.key)
         return {
-          ...targetInfo,
-          ...tgt,
-          hasSchedule: (targetInfo.schedule !== false),
-          syncInterval: tgt.syncInterval || targetInfo.schedule || 'P0D',
-          syncIntervalDefault: targetInfo.schedule,
-          config: _.sortBy(_.transform(tgt.config, (res, value, key) => {
-            const configData = _.get(targetInfo.props, key, false)
-            if (configData) {
-              res.push({
-                key,
-                value: JSON.stringify({
-                  ...configData,
-                  value: (configData.sensitive && value.length > 0) ? '********' : value
+          id: dbTarget?.id ?? uuid(),
+          isEnabled: dbTarget?.isEnabled ?? false,
+          module: md.key,
+          title: md.title,
+          description: md.description,
+          icon: md.icon,
+          banner: md.banner,
+          vendor: md.vendor,
+          website: md.website,
+          contentTypes: {
+            activeTypes: dbTarget?.contentTypes?.activeTypes ?? md.contentTypes.defaultTypesEnabled,
+            largeThreshold: dbTarget?.contentTypes?.largeThreshold ?? md.contentTypes.defaultLargeThreshold
+          },
+          assetDelivery: {
+            isStreamingSupported: md?.assetDelivery?.isStreamingSupported ?? false,
+            isDirectAccessSupported: md?.assetDelivery?.isDirectAccessSupported ?? false,
+            streaming: dbTarget?.assetDelivery?.streaming ?? md?.assetDelivery?.defaultStreamingEnabled ?? false,
+            directAccess: dbTarget?.assetDelivery?.directAccess ?? md?.assetDelivery?.defaultDirectAccessEnabled ?? false
+          },
+          versioning: {
+            isSupported: md?.versioning?.isSupported ?? false,
+            isForceEnabled: md?.versioning?.isForceEnabled ?? false,
+            enabled: dbTarget?.versioning?.enabled ?? md?.versioning?.defaultEnabled ?? false
+          },
+          sync: {},
+          status: {},
+          setup: {
+            handler: md?.setup?.handler,
+            state: dbTarget?.state?.setup ?? 'notconfigured',
+            values: md.setup?.handler
+              ? _.transform(md.setup.defaultValues,
+                (r, v, k) => {
+                  r[k] = dbTarget?.config?.[k] ?? v
+                }, {})
+              : {}
+          },
+          config: _.transform(md.props, (r, v, k) => {
+            const cfValue = dbTarget?.config?.[k] ?? v.default
+            r[k] = {
+              ...v,
+              value: v.sensitive && cfValue ? '********' : cfValue,
+              ...v.enum && {
+                enum: v.enum.map(o => {
+                  if (o.indexOf('|') > 0) {
+                    const oParsed = o.split('|')
+                    return {
+                      value: oParsed[0],
+                      label: oParsed[1]
+                    }
+                  } else {
+                    return {
+                      value: o,
+                      label: o
+                    }
+                  }
                 })
-              })
+              }
             }
-          }, []), 'key')
+          }, {}),
+          actions: md.actions
         }
-      }), ['title', 'key'])
-      return targets
-    },
-    async status(obj, args, context, info) {
-      let activeTargets = await WIKI.models.storage.query().where('isEnabled', true)
-      return activeTargets.map(tgt => {
-        const targetInfo = _.find(WIKI.data.storage, ['key', tgt.key]) || {}
-        return {
-          key: tgt.key,
-          title: targetInfo.title,
-          status: _.get(tgt, 'state.status', 'pending'),
-          message: _.get(tgt, 'state.message', 'Initializing...'),
-          lastAttempt: _.get(tgt, 'state.lastAttempt', null)
-        }
-      })
+      }), ['title'])
     }
   },
-  StorageMutation: {
-    async updateTargets(obj, args, context) {
+  Mutation: {
+    async updateStorageTargets (obj, args, context) {
+      WIKI.logger.debug(`Updating storage targets for site ${args.siteId}...`)
       try {
-        let dbTargets = await WIKI.models.storage.getTargets()
-        for (let tgt of args.targets) {
-          const currentDbTarget = _.find(dbTargets, ['key', tgt.key])
-          if (!currentDbTarget) {
-            continue
+        const dbTargets = await WIKI.models.storage.getTargets({ siteId: args.siteId })
+        for (const tgt of args.targets) {
+          const md = _.find(WIKI.storage.defs, ['key', tgt.module])
+          if (!md) {
+            throw new Error('Invalid module key for non-existent storage target.')
           }
-          await WIKI.models.storage.query().patch({
-            isEnabled: tgt.isEnabled,
-            mode: tgt.mode,
-            syncInterval: tgt.syncInterval,
-            config: _.reduce(tgt.config, (result, value, key) => {
-              let configValue = _.get(JSON.parse(value.value), 'v', null)
-              if (configValue === '********') {
-                configValue = _.get(currentDbTarget.config, value.key, '')
-              }
-              _.set(result, `${value.key}`, configValue)
-              return result
-            }, {}),
-            state: {
-              status: 'pending',
-              message: 'Initializing...',
-              lastAttempt: null
+
+          const dbTarget = _.find(dbTargets, ['id', tgt.id])
+
+          // -> Build update config object
+          const updatedConfig = dbTarget?.config ?? {}
+          if (tgt.config) {
+            for (const [key, prop] of Object.entries(md.props)) {
+              if (prop.readOnly) { continue }
+              if (!Object.prototype.hasOwnProperty.call(tgt.config, key)) { continue }
+              if (prop.sensitive && tgt.config[key] === '********') { continue }
+              updatedConfig[key] = tgt.config[key]
             }
-          }).where('key', tgt.key)
+          }
+
+          // -> Target doesn't exist yet in the DB, let's create it
+          if (!dbTarget) {
+            WIKI.logger.debug(`No existing DB configuration for module ${tgt.module}. Creating a new one...`)
+            await WIKI.models.storage.query().insert({
+              id: tgt.id,
+              module: tgt.module,
+              siteId: args.siteId,
+              isEnabled: tgt.isEnabled ?? false,
+              contentTypes: {
+                activeTypes: tgt.contentTypes ?? md.contentTypes.defaultTypesEnabled ?? [],
+                largeThreshold: tgt.largeThreshold ?? md.contentTypes.defaultLargeThreshold ?? '5MB'
+              },
+              assetDelivery: {
+                streaming: tgt.assetDeliveryFileStreaming ?? md?.assetDelivery?.defaultStreamingEnabled ?? false,
+                directAccess: tgt.assetDeliveryDirectAccess ?? md?.assetDelivery?.defaultDirectAccessEnabled ?? false
+              },
+              versioning: {
+                enabled: tgt.useVersioning ?? md?.versioning?.defaultEnabled ?? false
+              },
+              state: {
+                current: 'ok'
+              },
+              config: updatedConfig
+            })
+          } else {
+            WIKI.logger.debug(`Updating DB configuration for module ${tgt.module}...`)
+            await WIKI.models.storage.query().patch({
+              isEnabled: tgt.isEnabled ?? dbTarget.isEnabled ?? false,
+              contentTypes: {
+                activeTypes: tgt.contentTypes ?? dbTarget?.contentTypes?.activeTypes ?? [],
+                largeThreshold: tgt.largeThreshold ?? dbTarget?.contentTypes?.largeThreshold ?? '5MB'
+              },
+              assetDelivery: {
+                streaming: tgt.assetDeliveryFileStreaming ?? dbTarget?.assetDelivery?.streaming ?? false,
+                directAccess: tgt.assetDeliveryDirectAccess ?? dbTarget?.assetDelivery?.directAccess ?? false
+              },
+              versioning: {
+                enabled: tgt.useVersioning ?? dbTarget?.versioning?.enabled ?? false
+              },
+              config: updatedConfig
+            }).where('id', tgt.id)
+          }
+        }
+        // await WIKI.models.storage.initTargets()
+        return {
+          status: graphHelper.generateSuccess('Storage targets updated successfully')
+        }
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
+    },
+    async setupStorageTarget (obj, args, context) {
+      try {
+        const tgt = await WIKI.models.storage.query().findById(args.targetId)
+        if (!tgt) {
+          throw new Error('Not storage target matching this ID')
+        }
+        const md = _.find(WIKI.storage.defs, ['key', tgt.module])
+        if (!md) {
+          throw new Error('No matching storage module installed.')
+        }
+        if (!await WIKI.models.storage.ensureModule(md.key)) {
+          throw new Error('Failed to load storage module. Check logs for details.')
+        }
+        const result = await WIKI.storage.modules[md.key].setup(args.targetId, args.state)
+
+        return {
+          status: graphHelper.generateSuccess('Storage target setup step succeeded'),
+          state: result
         }
-        await WIKI.models.storage.initTargets()
+      } catch (err) {
+        return graphHelper.generateError(err)
+      }
+    },
+    async destroyStorageTargetSetup (obj, args, context) {
+      try {
+        const tgt = await WIKI.models.storage.query().findById(args.targetId)
+        if (!tgt) {
+          throw new Error('Not storage target matching this ID')
+        }
+        const md = _.find(WIKI.storage.defs, ['key', tgt.module])
+        if (!md) {
+          throw new Error('No matching storage module installed.')
+        }
+        if (!await WIKI.models.storage.ensureModule(md.key)) {
+          throw new Error('Failed to load storage module. Check logs for details.')
+        }
+        await WIKI.storage.modules[md.key].setupDestroy(args.targetId)
+
         return {
-          responseResult: graphHelper.generateSuccess('Storage targets updated successfully')
+          status: graphHelper.generateSuccess('Storage target setup configuration destroyed succesfully.')
         }
       } catch (err) {
         return graphHelper.generateError(err)
       }
     },
-    async executeAction(obj, args, context) {
+    async executeStorageAction (obj, args, context) {
       try {
         await WIKI.models.storage.executeAction(args.targetKey, args.handler)
         return {
-          responseResult: graphHelper.generateSuccess('Action completed.')
+          status: graphHelper.generateSuccess('Action completed.')
         }
       } catch (err) {
         return graphHelper.generateError(err)

+ 28 - 255
server/graph/resolvers/system.js

@@ -5,257 +5,55 @@ const os = require('os')
 const filesize = require('filesize')
 const path = require('path')
 const fs = require('fs-extra')
-const moment = require('moment')
+const { DateTime } = require('luxon')
 const graphHelper = require('../../helpers/graph')
-const request = require('request-promise')
-const crypto = require('crypto')
-const nanoid = require('nanoid/non-secure').customAlphabet('1234567890abcdef', 10)
 
 /* global WIKI */
 
-const dbTypes = {
-  mysql: 'MySQL',
-  mariadb: 'MariaDB',
-  postgres: 'PostgreSQL',
-  sqlite: 'SQLite',
-  mssql: 'MS SQL Server'
-}
-
 module.exports = {
   Query: {
-    async system () { return {} }
-  },
-  Mutation: {
-    async system () { return {} }
-  },
-  SystemQuery: {
-    flags () {
+    systemFlags () {
       return _.transform(WIKI.config.flags, (result, value, key) => {
         result.push({ key, value })
       }, [])
     },
-    async info () { return {} },
-    async extensions () {
-      const exts = Object.values(WIKI.extensions.ext).map(ext => _.pick(ext, ['key', 'title', 'description', 'isInstalled']))
-      for (let ext of exts) {
+    async systemInfo () { return {} },
+    async systemExtensions () {
+      const exts = Object.values(WIKI.extensions.ext).map(ext => _.pick(ext, ['key', 'title', 'description', 'isInstalled', 'isInstallable']))
+      for (const ext of exts) {
         ext.isCompatible = await WIKI.extensions.ext[ext.key].isCompatible()
       }
       return exts
+    },
+    systemSecurity () {
+      return WIKI.config.security
     }
   },
-  SystemMutation: {
-    async updateFlags (obj, args, context) {
+  Mutation: {
+    async updateSystemFlags (obj, args, context) {
       WIKI.config.flags = _.transform(args.flags, (result, row) => {
         _.set(result, row.key, row.value)
       }, {})
       await WIKI.configSvc.applyFlags()
       await WIKI.configSvc.saveToDb(['flags'])
       return {
-        responseResult: graphHelper.generateSuccess('System Flags applied successfully')
-      }
-    },
-    async resetTelemetryClientId (obj, args, context) {
-      try {
-        WIKI.telemetry.generateClientId()
-        await WIKI.configSvc.saveToDb(['telemetry'])
-        return {
-          responseResult: graphHelper.generateSuccess('Telemetry state updated successfully')
-        }
-      } catch (err) {
-        return graphHelper.generateError(err)
-      }
-    },
-    async setTelemetry (obj, args, context) {
-      try {
-        _.set(WIKI.config, 'telemetry.isEnabled', args.enabled)
-        WIKI.telemetry.enabled = args.enabled
-        await WIKI.configSvc.saveToDb(['telemetry'])
-        return {
-          responseResult: graphHelper.generateSuccess('Telemetry Client ID has been reset successfully')
-        }
-      } catch (err) {
-        return graphHelper.generateError(err)
-      }
-    },
-    async performUpgrade (obj, args, context) {
-      try {
-        if (process.env.UPGRADE_COMPANION) {
-          await request({
-            method: 'POST',
-            uri: 'http://wiki-update-companion/upgrade'
-          })
-          return {
-            responseResult: graphHelper.generateSuccess('Upgrade has started.')
-          }
-        } else {
-          throw new Error('You must run the wiki-update-companion container and pass the UPGRADE_COMPANION env var in order to use this feature.')
-        }
-      } catch (err) {
-        return graphHelper.generateError(err)
-      }
-    },
-    /**
-     * Import Users from a v1 installation
-     */
-    async importUsersFromV1(obj, args, context) {
-      try {
-        const MongoClient = require('mongodb').MongoClient
-        if (args.mongoDbConnString && args.mongoDbConnString.length > 10) {
-          // -> Connect to DB
-
-          const client = await MongoClient.connect(args.mongoDbConnString, {
-            appname: `Wiki.js ${WIKI.version} Migration Tool`
-          })
-          const dbUsers = client.db().collection('users')
-          const userCursor = dbUsers.find({ email: { '$ne': 'guest' } })
-
-          const curDateISO = new Date().toISOString()
-
-          let failed = []
-          let usersCount = 0
-          let groupsCount = 0
-          let assignableGroups = []
-          let reuseGroups = []
-
-          // -> Create SINGLE group
-
-          if (args.groupMode === `SINGLE`) {
-            const singleGroup = await WIKI.models.groups.query().insert({
-              name: `Import_${curDateISO}`,
-              permissions: JSON.stringify(WIKI.data.groups.defaultPermissions),
-              pageRules: JSON.stringify(WIKI.data.groups.defaultPageRules)
-            })
-            groupsCount++
-            assignableGroups.push(singleGroup.id)
-          }
-
-          // -> Iterate all users
-
-          while (await userCursor.hasNext()) {
-            const usr = await userCursor.next()
-
-            let usrGroup = []
-            if (args.groupMode === `MULTI`) {
-              // -> Check if global admin
-
-              if (_.some(usr.rights, ['role', 'admin'])) {
-                usrGroup.push(1)
-              } else {
-                // -> Check if identical group already exists
-
-                const currentRights = _.sortBy(_.map(usr.rights, r => _.pick(r, ['role', 'path', 'exact', 'deny'])), ['role', 'path', 'exact', 'deny'])
-                const ruleSetId = crypto.createHash('sha1').update(JSON.stringify(currentRights)).digest('base64')
-                const existingGroup = _.find(reuseGroups, ['hash', ruleSetId])
-                if (existingGroup) {
-                  usrGroup.push(existingGroup.groupId)
-                } else {
-                  // -> Build new group
-
-                  const pageRules = _.map(usr.rights, r => {
-                    let roles = ['read:pages', 'read:assets', 'read:comments', 'write:comments']
-                    if (r.role === `write`) {
-                      roles = _.concat(roles, ['write:pages', 'manage:pages', 'read:source', 'read:history', 'write:assets', 'manage:assets'])
-                    }
-                    return {
-                      id: nanoid(),
-                      roles: roles,
-                      match: r.exact ? 'EXACT' : 'START',
-                      deny: r.deny,
-                      path: (r.path.indexOf('/') === 0) ? r.path.substring(1) : r.path,
-                      locales: []
-                    }
-                  })
-
-                  const perms = _.chain(pageRules).reject('deny').map('roles').union().flatten().value()
-
-                  // -> Create new group
-
-                  const newGroup = await WIKI.models.groups.query().insert({
-                    name: `Import_${curDateISO}_${groupsCount + 1}`,
-                    permissions: JSON.stringify(perms),
-                    pageRules: JSON.stringify(pageRules)
-                  })
-                  reuseGroups.push({
-                    groupId: newGroup.id,
-                    hash: ruleSetId
-                  })
-                  groupsCount++
-                  usrGroup.push(newGroup.id)
-                }
-              }
-            }
-
-            // -> Create User
-
-            try {
-              await WIKI.models.users.createNewUser({
-                providerKey: usr.provider,
-                email: usr.email,
-                name: usr.name,
-                passwordRaw: usr.password,
-                groups: (usrGroup.length > 0) ? usrGroup : assignableGroups,
-                mustChangePassword: false,
-                sendWelcomeEmail: false
-              })
-              usersCount++
-            } catch (err) {
-              failed.push({
-                provider: usr.provider,
-                email: usr.email,
-                error: err.message
-              })
-              WIKI.logger.warn(`${usr.email}: ${err}`)
-            }
-          }
-
-          // -> Reload group permissions
-
-          if (args.groupMode !== `NONE`) {
-            await WIKI.auth.reloadGroups()
-            WIKI.events.outbound.emit('reloadGroups')
-          }
-
-          client.close()
-          return {
-            responseResult: graphHelper.generateSuccess('Import completed.'),
-            usersCount: usersCount,
-            groupsCount: groupsCount,
-            failed: failed
-          }
-        } else {
-          throw new Error('MongoDB Connection String is missing or invalid.')
-        }
-      } catch (err) {
-        return graphHelper.generateError(err)
+        status: graphHelper.generateSuccess('System Flags applied successfully')
       }
     },
-    /**
-     * Set HTTPS Redirection State
-     */
-    async setHTTPSRedirection (obj, args, context) {
-      _.set(WIKI.config, 'server.sslRedir', args.enabled)
-      await WIKI.configSvc.saveToDb(['server'])
+    async updateSystemSecurity (obj, args, context) {
+      WIKI.config.security = _.defaultsDeep(_.omit(args, ['__typename']), WIKI.config.security)
+      // TODO: broadcast config update
+      await WIKI.configSvc.saveToDb(['security'])
       return {
-        responseResult: graphHelper.generateSuccess('HTTP Redirection state set successfully.')
+        status: graphHelper.generateSuccess('System Security configuration applied successfully')
       }
     },
-    /**
-     * Renew SSL Certificate
-     */
-    async renewHTTPSCertificate (obj, args, context) {
+    async installExtension (obj, args, context) {
       try {
-        if (!WIKI.config.ssl.enabled) {
-          throw new WIKI.Error.SystemSSLDisabled()
-        } else if (WIKI.config.ssl.provider !== `letsencrypt`) {
-          throw new WIKI.Error.SystemSSLRenewInvalidProvider()
-        } else if (!WIKI.servers.le) {
-          throw new WIKI.Error.SystemSSLLEUnavailable()
-        } else {
-          await WIKI.servers.le.requestCertificate()
-          await WIKI.servers.restartServer('https')
-          return {
-            responseResult: graphHelper.generateSuccess('SSL Certificate renewed successfully.')
-          }
+        await WIKI.extensions.ext[args.key].install()
+        // TODO: broadcast ext install
+        return {
+          status: graphHelper.generateSuccess('Extension installed successfully')
         }
       } catch (err) {
         return graphHelper.generateError(err)
@@ -272,36 +70,11 @@ module.exports = {
     currentVersion () {
       return WIKI.version
     },
-    dbType () {
-      return _.get(dbTypes, WIKI.config.db.type, 'Unknown DB')
-    },
     async dbVersion () {
-      let version = 'Unknown Version'
-      switch (WIKI.config.db.type) {
-        case 'mariadb':
-        case 'mysql':
-          const resultMYSQL = await WIKI.models.knex.raw('SELECT VERSION() as version;')
-          version = _.get(resultMYSQL, '[0][0].version', 'Unknown Version')
-          break
-        case 'mssql':
-          const resultMSSQL = await WIKI.models.knex.raw('SELECT @@VERSION as version;')
-          version = _.get(resultMSSQL, '[0].version', 'Unknown Version')
-          break
-        case 'postgres':
-          version = _.get(WIKI.models, 'knex.client.version', 'Unknown Version')
-          break
-        case 'sqlite':
-          version = _.get(WIKI.models, 'knex.client.driver.VERSION', 'Unknown Version')
-          break
-      }
-      return version
+      return _.get(WIKI.models, 'knex.client.version', 'Unknown Version')
     },
     dbHost () {
-      if (WIKI.config.db.type === 'sqlite') {
-        return WIKI.config.db.storage
-      } else {
-        return WIKI.config.db.host
-      }
+      return WIKI.config.db.host
     },
     hostname () {
       return os.hostname()
@@ -319,7 +92,7 @@ module.exports = {
       return WIKI.system.updates.version
     },
     latestVersionReleaseDate () {
-      return moment.utc(WIKI.system.updates.releaseDate)
+      return DateTime.fromISO(WIKI.system.updates.releaseDate).toJSDate()
     },
     nodeVersion () {
       return process.version.substr(1)
@@ -343,10 +116,10 @@ module.exports = {
       return filesize(os.totalmem())
     },
     sslDomain () {
-      return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === `letsencrypt` ? WIKI.config.ssl.domain : null
+      return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === 'letsencrypt' ? WIKI.config.ssl.domain : null
     },
     sslExpirationDate () {
-      return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === `letsencrypt` ? _.get(WIKI.config.letsencrypt, 'payload.expires', null) : null
+      return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === 'letsencrypt' ? _.get(WIKI.config.letsencrypt, 'payload.expires', null) : null
     },
     sslProvider () {
       return WIKI.config.ssl.enabled ? WIKI.config.ssl.provider : null
@@ -355,7 +128,7 @@ module.exports = {
       return 'OK'
     },
     sslSubscriberEmail () {
-      return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === `letsencrypt` ? WIKI.config.ssl.subscriberEmail : null
+      return WIKI.config.ssl.enabled && WIKI.config.ssl.provider === 'letsencrypt' ? WIKI.config.ssl.subscriberEmail : null
     },
     telemetry () {
       return WIKI.telemetry.enabled

+ 0 - 58
server/graph/resolvers/tag.js

@@ -1,58 +0,0 @@
-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()
-  //   }
-  // }
-}

+ 0 - 62
server/graph/resolvers/theming.js

@@ -1,62 +0,0 @@
-const graphHelper = require('../../helpers/graph')
-const _ = require('lodash')
-const CleanCSS = require('clean-css')
-
-/* global WIKI */
-
-module.exports = {
-  Query: {
-    async theming() { return {} }
-  },
-  Mutation: {
-    async theming() { return {} }
-  },
-  ThemingQuery: {
-    async themes(obj, args, context, info) {
-      return [{ // TODO
-        key: 'default',
-        title: 'Default',
-        author: 'requarks.io'
-      }]
-    },
-    async config(obj, args, context, info) {
-      return {
-        theme: WIKI.config.theming.theme,
-        iconset: WIKI.config.theming.iconset,
-        darkMode: WIKI.config.theming.darkMode,
-        injectCSS: new CleanCSS({ format: 'beautify' }).minify(WIKI.config.theming.injectCSS).styles,
-        injectHead: WIKI.config.theming.injectHead,
-        injectBody: WIKI.config.theming.injectBody
-      }
-    }
-  },
-  ThemingMutation: {
-    async setConfig(obj, args, context, info) {
-      try {
-        if (!_.isEmpty(args.injectCSS)) {
-          args.injectCSS = new CleanCSS({
-            inline: false
-          }).minify(args.injectCSS).styles
-        }
-
-        WIKI.config.theming = {
-          ...WIKI.config.theming,
-          theme: args.theme,
-          iconset: args.iconset,
-          darkMode: args.darkMode,
-          injectCSS: args.injectCSS || '',
-          injectHead: args.injectHead || '',
-          injectBody: args.injectBody || ''
-        }
-
-        await WIKI.configSvc.saveToDb(['theming'])
-
-        return {
-          responseResult: graphHelper.generateSuccess('Theme config updated')
-        }
-      } catch (err) {
-        return graphHelper.generateError(err)
-      }
-    }
-  }
-}

+ 66 - 44
server/graph/resolvers/user.js

@@ -5,32 +5,54 @@ const _ = require('lodash')
 
 module.exports = {
   Query: {
-    async users() { return {} }
-  },
-  Mutation: {
-    async users() { return {} }
-  },
-  UserQuery: {
-    async list(obj, args, context, info) {
-      return WIKI.models.users.query()
-        .select('id', 'email', 'name', 'providerKey', 'isSystem', 'isActive', 'createdAt', 'lastLoginAt')
-    },
-    async search(obj, args, context, info) {
+    /**
+     * FETCH ALL USERS
+     */
+    async users (obj, args, context, info) {
+      // -> Sanitize limit
+      let limit = args.pageSize ?? 20
+      if (limit < 1 || limit > 1000) {
+        limit = 1000
+      }
+
+      // -> Sanitize offset
+      let offset = args.page ?? 1
+      if (offset < 1) {
+        offset = 1
+      }
+
+      // -> Fetch Users
       return WIKI.models.users.query()
-        .where('email', 'like', `%${args.query}%`)
-        .orWhere('name', 'like', `%${args.query}%`)
-        .limit(10)
-        .select('id', 'email', 'name', 'providerKey', 'createdAt')
+        .select('id', 'email', 'name', 'isSystem', 'isActive', 'createdAt', 'lastLoginAt')
+        .where(builder => {
+          if (args.filter) {
+            builder.where('email', 'like', `%${args.filter}%`)
+              .orWhere('name', 'like', `%${args.filter}%`)
+          }
+        })
+        .orderBy(args.orderBy ?? 'name', args.orderByDirection ?? 'asc')
+        .offset((offset - 1) * limit)
+        .limit(limit)
     },
-    async single(obj, args, context, info) {
-      let usr = await WIKI.models.users.query().findById(args.id)
-      usr.password = ''
-      usr.tfaSecret = ''
+    /**
+     * FETCH A SINGLE USER
+     */
+    async userById (obj, args, context, info) {
+      const usr = await WIKI.models.users.query().findById(args.id)
+
+      // const str = _.get(WIKI.auth.strategies, usr.providerKey)
+      // str.strategy = _.find(WIKI.data.authentication, ['key', str.strategyKey])
+      // usr.providerName = str.displayName
+      // usr.providerIs2FACapable = _.get(str, 'strategy.useForm', false)
 
-      const str = _.get(WIKI.auth.strategies, usr.providerKey)
-      str.strategy = _.find(WIKI.data.authentication, ['key', str.strategyKey])
-      usr.providerName = str.displayName
-      usr.providerIs2FACapable = _.get(str, 'strategy.useForm', false)
+      usr.auth = _.mapValues(usr.auth, (auth, providerKey) => {
+        if (auth.password) {
+          auth.password = '***'
+        }
+        auth.module = providerKey === '00910749-8ab6-498a-9be0-f4ca28ea5e52' ? 'google' : 'local'
+        auth._moduleName = providerKey === '00910749-8ab6-498a-9be0-f4ca28ea5e52' ? 'Google' : 'Local'
+        return auth
+      })
 
       return usr
     },
@@ -61,19 +83,19 @@ module.exports = {
         .limit(10)
     }
   },
-  UserMutation: {
-    async create (obj, args) {
+  Mutation: {
+    async createUser (obj, args) {
       try {
-        await WIKI.models.users.createNewUser(args)
+        await WIKI.models.users.createNewUser({ ...args, passwordRaw: args.password, isVerified: true })
 
         return {
-          responseResult: graphHelper.generateSuccess('User created successfully')
+          status: graphHelper.generateSuccess('User created successfully')
         }
       } catch (err) {
         return graphHelper.generateError(err)
       }
     },
-    async delete (obj, args) {
+    async deleteUser (obj, args) {
       try {
         if (args.id <= 2) {
           throw new WIKI.Error.UserDeleteProtected()
@@ -84,7 +106,7 @@ module.exports = {
         WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })
 
         return {
-          responseResult: graphHelper.generateSuccess('User deleted successfully')
+          status: graphHelper.generateSuccess('User deleted successfully')
         }
       } catch (err) {
         if (err.message.indexOf('foreign') >= 0) {
@@ -94,40 +116,40 @@ module.exports = {
         }
       }
     },
-    async update (obj, args) {
+    async updateUser (obj, args) {
       try {
-        await WIKI.models.users.updateUser(args)
+        await WIKI.models.users.updateUser(args.id, args.patch)
 
         return {
-          responseResult: graphHelper.generateSuccess('User created successfully')
+          status: graphHelper.generateSuccess('User updated successfully')
         }
       } catch (err) {
         return graphHelper.generateError(err)
       }
     },
-    async verify (obj, args) {
+    async verifyUser (obj, args) {
       try {
         await WIKI.models.users.query().patch({ isVerified: true }).findById(args.id)
 
         return {
-          responseResult: graphHelper.generateSuccess('User verified successfully')
+          status: graphHelper.generateSuccess('User verified successfully')
         }
       } catch (err) {
         return graphHelper.generateError(err)
       }
     },
-    async activate (obj, args) {
+    async activateUser (obj, args) {
       try {
         await WIKI.models.users.query().patch({ isActive: true }).findById(args.id)
 
         return {
-          responseResult: graphHelper.generateSuccess('User activated successfully')
+          status: graphHelper.generateSuccess('User activated successfully')
         }
       } catch (err) {
         return graphHelper.generateError(err)
       }
     },
-    async deactivate (obj, args) {
+    async deactivateUser (obj, args) {
       try {
         if (args.id <= 2) {
           throw new Error('Cannot deactivate system accounts.')
@@ -138,35 +160,35 @@ module.exports = {
         WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })
 
         return {
-          responseResult: graphHelper.generateSuccess('User deactivated successfully')
+          status: graphHelper.generateSuccess('User deactivated successfully')
         }
       } catch (err) {
         return graphHelper.generateError(err)
       }
     },
-    async enableTFA (obj, args) {
+    async enableUserTFA (obj, args) {
       try {
         await WIKI.models.users.query().patch({ tfaIsActive: true, tfaSecret: null }).findById(args.id)
 
         return {
-          responseResult: graphHelper.generateSuccess('User 2FA enabled successfully')
+          status: graphHelper.generateSuccess('User 2FA enabled successfully')
         }
       } catch (err) {
         return graphHelper.generateError(err)
       }
     },
-    async disableTFA (obj, args) {
+    async disableUserTFA (obj, args) {
       try {
         await WIKI.models.users.query().patch({ tfaIsActive: false, tfaSecret: null }).findById(args.id)
 
         return {
-          responseResult: graphHelper.generateSuccess('User 2FA disabled successfully')
+          status: graphHelper.generateSuccess('User 2FA disabled successfully')
         }
       } catch (err) {
         return graphHelper.generateError(err)
       }
     },
-    resetPassword (obj, args) {
+    resetUserPassword (obj, args) {
       return false
     },
     async updateProfile (obj, args, context) {
@@ -203,7 +225,7 @@ module.exports = {
         const newToken = await WIKI.models.users.refreshToken(usr.id)
 
         return {
-          responseResult: graphHelper.generateSuccess('User profile updated successfully'),
+          status: graphHelper.generateSuccess('User profile updated successfully'),
           jwt: newToken.token
         }
       } catch (err) {

+ 15 - 18
server/graph/scalars/date.js

@@ -1,21 +1,18 @@
-
 const gql = require('graphql')
 
-module.exports = {
-  Date: new gql.GraphQLScalarType({
-    name: 'Date',
-    description: 'ISO date-time string at UTC',
-    parseValue(value) {
-      return new Date(value)
-    },
-    serialize(value) {
-      return value.toISOString()
-    },
-    parseLiteral(ast) {
-      if (ast.kind !== gql.Kind.STRING) {
-        throw new TypeError('Date value must be an string!')
-      }
-      return new Date(ast.value)
+module.exports = new gql.GraphQLScalarType({
+  name: 'Date',
+  description: 'ISO date-time string at UTC',
+  parseValue(value) {
+    return new Date(value)
+  },
+  serialize(value) {
+    return value.toISOString()
+  },
+  parseLiteral(ast) {
+    if (ast.kind !== gql.Kind.STRING) {
+      throw new TypeError('Date value must be an string!')
     }
-  })
-}
+    return new Date(ast.value)
+  }
+})

+ 15 - 17
server/graph/scalars/json.js

@@ -39,21 +39,19 @@ function parseObject (typeName, ast, variables) {
   return value
 }
 
-module.exports = {
-  JSON: new GraphQLScalarType({
-    name: 'JSON',
-    description:
-      'The `JSON` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).',
-    specifiedByUrl:
-      'http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf',
-    serialize: ensureObject,
-    parseValue: ensureObject,
-    parseLiteral: (ast, variables) => {
-      if (ast.kind !== Kind.OBJECT) {
-        throw new TypeError(`JSONObject cannot represent non-object value: ${ast}`)
-      }
-
-      return parseObject('JSONObject', ast, variables)
+module.exports = new GraphQLScalarType({
+  name: 'JSON',
+  description:
+    'The `JSON` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).',
+  specifiedByUrl:
+    'http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf',
+  serialize: ensureObject,
+  parseValue: ensureObject,
+  parseLiteral: (ast, variables) => {
+    if (ast.kind !== Kind.OBJECT) {
+      throw new TypeError(`JSONObject cannot represent non-object value: ${ast}`)
     }
-  })
-}
+
+    return parseObject('JSONObject', ast, variables)
+  }
+})

+ 23 - 26
server/graph/scalars/uuid.js

@@ -1,5 +1,4 @@
 const { Kind, GraphQLScalarType } = require('graphql')
-// const { Kind } = require('graphql/language')
 
 const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
 const nilUUID = '00000000-0000-0000-0000-000000000000'
@@ -8,32 +7,30 @@ function isUUID (value) {
   return uuidRegex.test(value) || nilUUID === value
 }
 
-module.exports = {
-  UUID: new GraphQLScalarType({
-    name: 'UUID',
-    description: 'The `UUID` scalar type represents UUID values as specified by [RFC 4122](https://tools.ietf.org/html/rfc4122).',
-    serialize: (value) => {
-      if (!isUUID(value)) {
-        throw new TypeError(`UUID cannot represent non-UUID value: ${value}`)
-      }
+module.exports = new GraphQLScalarType({
+  name: 'UUID',
+  description: 'The `UUID` scalar type represents UUID values as specified by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122).',
+  serialize: (value) => {
+    if (!isUUID(value)) {
+      throw new TypeError(`UUID cannot represent non-UUID value: ${value}`)
+    }
 
-      return value.toLowerCase()
-    },
-    parseValue: (value) => {
-      if (!isUUID(value)) {
-        throw new TypeError(`UUID cannot represent non-UUID value: ${value}`)
-      }
+    return value.toLowerCase()
+  },
+  parseValue: (value) => {
+    if (!isUUID(value)) {
+      throw new TypeError(`UUID cannot represent non-UUID value: ${value}`)
+    }
 
-      return value.toLowerCase()
-    },
-    parseLiteral: (ast) => {
-      if (ast.kind === Kind.STRING) {
-        if (isUUID(ast.value)) {
-          return ast.value
-        }
+    return value.toLowerCase()
+  },
+  parseLiteral: (ast) => {
+    if (ast.kind === Kind.STRING) {
+      if (isUUID(ast.value)) {
+        return ast.value
       }
-
-      return undefined
     }
-  })
-}
+
+    return undefined
+  }
+})

+ 5 - 27
server/graph/schemas/analytics.graphql

@@ -3,45 +3,23 @@
 # ===============================================
 
 extend type Query {
-  analytics: AnalyticsQuery
-}
-
-extend type Mutation {
-  analytics: AnalyticsMutation
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
-
-"""
-Queries for Analytics
-"""
-type AnalyticsQuery {
   """
   Fetch list of Analytics providers and their configuration
   """
-  providers(
+  analyticsProviders(
     "Return only active providers"
     isEnabled: Boolean
-  ): [AnalyticsProvider] @auth(requires: ["manage:system"])
+  ): [AnalyticsProvider]
 }
 
-# -----------------------------------------------
-# MUTATIONS
-# -----------------------------------------------
-
-"""
-Mutations for Analytics
-"""
-type AnalyticsMutation {
+extend type Mutation {
   """
   Update a list of Analytics providers and their configuration
   """
-  updateProviders(
+  updateAnalyticsProviders(
     "List of providers"
     providers: [AnalyticsProviderInput]!
-  ): DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
 }
 
 # -----------------------------------------------

+ 10 - 26
server/graph/schemas/asset.graphql

@@ -3,49 +3,33 @@
 # ===============================================
 
 extend type Query {
-  assets: AssetQuery
-}
-
-extend type Mutation {
-  assets: AssetMutation
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
-
-type AssetQuery {
-  list(
+  assets(
     folderId: Int!
     kind: AssetKind!
-  ): [AssetItem] @auth(requires: ["manage:system", "read:assets"])
+  ): [AssetItem]
 
-  folders(
+  assetsFolders(
     parentFolderId: Int!
-  ): [AssetFolder] @auth(requires: ["manage:system", "read:assets"])
+  ): [AssetFolder]
 }
 
-# -----------------------------------------------
-# MUTATIONS
-# -----------------------------------------------
-
-type AssetMutation {
-  createFolder(
+extend type Mutation {
+  createAssetsFolder(
     parentFolderId: Int!
     slug: String!
     name: String
-  ): DefaultResponse @auth(requires: ["manage:system", "write:assets"])
+  ): DefaultResponse
 
   renameAsset(
     id: Int!
     filename: String!
-  ): DefaultResponse @auth(requires: ["manage:system", "manage:assets"])
+  ): DefaultResponse
 
   deleteAsset(
     id: Int!
-  ): DefaultResponse @auth(requires: ["manage:system", "manage:assets"])
+  ): DefaultResponse
 
-  flushTempUploads: DefaultResponse @auth(requires: ["manage:system"])
+  flushTempUploads: DefaultResponse
 }
 
 # -----------------------------------------------

+ 15 - 33
server/graph/schemas/authentication.graphql

@@ -3,40 +3,22 @@
 # ===============================================
 
 extend type Query {
-  authentication: AuthenticationQuery
-}
-
-extend type Mutation {
-  authentication: AuthenticationMutation
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
+  apiKeys: [AuthenticationApiKey]
 
-type AuthenticationQuery {
-  apiKeys: [AuthenticationApiKey] @auth(requires: ["manage:system", "manage:api"])
+  apiState: Boolean
 
-  apiState: Boolean! @auth(requires: ["manage:system", "manage:api"])
-
-  strategies: [AuthenticationStrategy] @auth(requires: ["manage:system"])
-
-  activeStrategies(
+  authStrategies(
     enabledOnly: Boolean
-  ): [AuthenticationActiveStrategy]
+  ): [AuthenticationStrategy]
 }
 
-# -----------------------------------------------
-# MUTATIONS
-# -----------------------------------------------
-
-type AuthenticationMutation {
+extend type Mutation {
   createApiKey(
     name: String!
     expiration: String!
     fullAccess: Boolean!
     group: Int
-  ): AuthenticationCreateApiKeyResponse @auth(requires: ["manage:system", "manage:api"])
+  ): AuthenticationCreateApiKeyResponse
 
   login(
     username: String!
@@ -67,19 +49,19 @@ type AuthenticationMutation {
 
   revokeApiKey(
     id: Int!
-  ): DefaultResponse @auth(requires: ["manage:system", "manage:api"])
+  ): DefaultResponse
 
   setApiState(
     enabled: Boolean!
-  ): DefaultResponse @auth(requires: ["manage:system", "manage:api"])
+  ): DefaultResponse
 
-  updateStrategies(
+  updateAuthStrategies(
     strategies: [AuthenticationStrategyInput]!
-  ): DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
 
-  regenerateCertificates: DefaultResponse @auth(requires: ["manage:system"])
+  regenerateCertificates: DefaultResponse
 
-  resetGuestUser: DefaultResponse @auth(requires: ["manage:system"])
+  resetGuestUser: DefaultResponse
 }
 
 # -----------------------------------------------
@@ -113,7 +95,7 @@ type AuthenticationActiveStrategy {
 }
 
 type AuthenticationLoginResponse {
-  responseResult: ResponseStatus
+  operation: Operation
   jwt: String
   mustChangePwd: Boolean
   mustProvideTFA: Boolean
@@ -124,7 +106,7 @@ type AuthenticationLoginResponse {
 }
 
 type AuthenticationRegisterResponse {
-  responseResult: ResponseStatus
+  operation: Operation
   jwt: String
 }
 
@@ -151,6 +133,6 @@ type AuthenticationApiKey {
 }
 
 type AuthenticationCreateApiKeyResponse {
-  responseResult: ResponseStatus
+  operation: Operation
   key: String
 }

+ 28 - 44
server/graph/schemas/comment.graphql

@@ -3,55 +3,39 @@
 # ===============================================
 
 extend type Query {
-  comments: CommentQuery
-}
-
-extend type Mutation {
-  comments: CommentMutation
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
-
-type CommentQuery {
-  providers: [CommentProvider] @auth(requires: ["manage:system"])
+  commentsProviders: [CommentProvider]
 
-  list(
+  comments(
     locale: String!
     path: String!
-  ): [CommentPost]! @auth(requires: ["read:comments", "manage:system"])
+  ): [CommentPost]!
 
-  single(
+  commentById(
     id: Int!
-  ): CommentPost @auth(requires: ["read:comments", "manage:system"])
+  ): CommentPost
 }
 
-# -----------------------------------------------
-# MUTATIONS
-# -----------------------------------------------
-
-type CommentMutation {
-  updateProviders(
+extend type Mutation {
+  updateCommentsProviders(
     providers: [CommentProviderInput]
-  ): DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
 
-  create(
+  createComment(
     pageId: Int!
     replyTo: Int
     content: String!
     guestName: String
     guestEmail: String
-  ): CommentCreateResponse @auth(requires: ["write:comments", "manage:system"]) @rateLimit(limit: 1, duration: 15)
+  ): CommentCreateResponse  @rateLimit(limit: 1, duration: 15)
 
-  update(
+  updateComment(
     id: Int!
     content: String!
-  ): CommentUpdateResponse @auth(requires: ["write:comments", "manage:comments", "manage:system"])
+  ): CommentUpdateResponse
 
-  delete(
+  deleteComment(
     id: Int!
-  ): DefaultResponse @auth(requires: ["manage:comments", "manage:system"])
+  ): DefaultResponse
 }
 
 # -----------------------------------------------
@@ -59,9 +43,9 @@ type CommentMutation {
 # -----------------------------------------------
 
 type CommentProvider {
-  isEnabled: Boolean!
-  key: String!
-  title: String!
+  isEnabled: Boolean
+  key: String
+  title: String
   description: String
   logo: String
   website: String
@@ -76,23 +60,23 @@ input CommentProviderInput {
 }
 
 type CommentPost {
-  id: Int!
-  content: String! @auth(requires: ["write:comments", "manage:comments", "manage:system"])
-  render: String!
-  authorId: Int!
-  authorName: String!
-  authorEmail: String! @auth(requires: ["manage:system"])
-  authorIP: String! @auth(requires: ["manage:system"])
-  createdAt: Date!
-  updatedAt: Date!
+  id: Int
+  content: String
+  render: String
+  authorId: Int
+  authorName: String
+  authorEmail: String
+  authorIP: String
+  createdAt: Date
+  updatedAt: Date
 }
 
 type CommentCreateResponse {
-  responseResult: ResponseStatus
+  operation: Operation
   id: Int
 }
 
 type CommentUpdateResponse {
-  responseResult: ResponseStatus
+  operation: Operation
   render: String
 }

+ 8 - 12
server/graph/schemas/common.graphql

@@ -2,7 +2,7 @@
 # Wiki.js GraphQL Schema #
 # ====================== #
 
-# DIRECTIVES
+# DIRECTIVES (deprecated)
 # ----------
 
 directive @auth(requires: [String]) on QUERY | FIELD_DEFINITION | ARGUMENT_DEFINITION
@@ -12,8 +12,8 @@ directive @auth(requires: [String]) on QUERY | FIELD_DEFINITION | ARGUMENT_DEFIN
 
 # Generic Key Value Pair
 type KeyValuePair {
-  key: String!
-  value: String!
+  key: String
+  value: String
 }
 # General Key Value Pair Input
 input KeyValuePairInput {
@@ -23,14 +23,13 @@ input KeyValuePairInput {
 
 # Generic Mutation Response
 type DefaultResponse {
-  responseResult: ResponseStatus
+  operation: Operation
 }
 
-# Mutation Status
-type ResponseStatus {
-  succeeded: Boolean!
-  errorCode: Int!
-  slug: String!
+# Mutation Operation
+type Operation {
+  succeeded: Boolean
+  slug: String
   message: String
 }
 
@@ -47,6 +46,3 @@ type Query
 
 # Mutations (Create, Update, Delete)
 type Mutation
-
-# Subscriptions (Push, Real-time)
-type Subscription

+ 0 - 29
server/graph/schemas/contribute.graphql

@@ -1,29 +0,0 @@
-# ===============================================
-# CONTRIBUTE
-# ===============================================
-
-extend type Query {
-  contribute: ContributeQuery
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
-
-type ContributeQuery {
-  contributors: [ContributeContributor]
-}
-
-# -----------------------------------------------
-# TYPES
-# -----------------------------------------------
-
-type ContributeContributor {
-  id: String!
-  source: String!
-  name: String!
-  joined: Date!
-  website: String
-  twitter: String
-  avatar: String
-}

+ 36 - 57
server/graph/schemas/group.graphql

@@ -3,58 +3,39 @@
 # ===============================================
 
 extend type Query {
-  groups: GroupQuery
-}
-
-extend type Mutation {
-  groups: GroupMutation
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
-
-type GroupQuery {
-  list(
+  groups(
     filter: String
     orderBy: String
-  ): [GroupMinimal] @auth(requires: ["write:groups", "manage:groups", "manage:system"])
+  ): [Group]
 
-  single(
+  groupById(
     id: Int!
-  ): Group @auth(requires: ["write:groups", "manage:groups", "manage:system"])
+  ): Group
 }
 
-# -----------------------------------------------
-# MUTATIONS
-# -----------------------------------------------
-
-type GroupMutation {
-  create(
+extend type Mutation {
+  createGroup(
     name: String!
-  ): GroupResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"])
+  ): GroupResponse
 
-  update(
+  updateGroup(
     id: Int!
-    name: String!
-    redirectOnLogin: String!
-    permissions: [String]!
-    pageRules: [PageRuleInput]!
-  ): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"])
+    patch: GroupUpdateInput!
+  ): DefaultResponse
 
-  delete(
+  deleteGroup(
     id: Int!
-  ): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"])
+  ): DefaultResponse
 
-  assignUser(
+  assignUserToGroup(
     groupId: Int!
     userId: Int!
-  ): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"])
+  ): DefaultResponse
 
-  unassignUser(
+  unassignUserFromGroup(
     groupId: Int!
     userId: Int!
-  ): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"])
+  ): DefaultResponse
 }
 
 # -----------------------------------------------
@@ -62,38 +43,36 @@ type GroupMutation {
 # -----------------------------------------------
 
 type GroupResponse {
-  responseResult: ResponseStatus!
+  operation: Operation
   group: Group
 }
 
-type GroupMinimal {
-  id: Int!
-  name: String!
-  isSystem: Boolean!
-  userCount: Int
-  createdAt: Date!
-  updatedAt: Date!
-}
-
 type Group {
-  id: Int!
-  name: String!
-  isSystem: Boolean!
+  id: Int
+  name: String
+  isSystem: Boolean
   redirectOnLogin: String
-  permissions: [String]!
+  permissions: [String]
   pageRules: [PageRule]
   users: [UserMinimal]
-  createdAt: Date!
-  updatedAt: Date!
+  createdAt: Date
+  updatedAt: Date
 }
 
 type PageRule {
-  id: String!
-  deny: Boolean!
-  match: PageRuleMatch!
-  roles: [String]!
-  path: String!
-  locales: [String]!
+  id: String
+  deny: Boolean
+  match: PageRuleMatch
+  roles: [String]
+  path: String
+  locales: [String]
+}
+
+input GroupUpdateInput {
+  name: String!
+  redirectOnLogin: String!
+  permissions: [String]!
+  pageRules: [PageRuleInput]!
 }
 
 input PageRuleInput {

+ 13 - 37
server/graph/schemas/localization.graphql

@@ -3,38 +3,21 @@
 # ===============================================
 
 extend type Query {
-  localization: LocalizationQuery
-}
-
-extend type Mutation {
-  localization: LocalizationMutation
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
-
-type LocalizationQuery {
   locales: [LocalizationLocale]
-  config: LocalizationConfig
   translations(locale: String!, namespace: String!): [Translation]
 }
 
-# -----------------------------------------------
-# MUTATIONS
-# -----------------------------------------------
-
-type LocalizationMutation {
+extend type Mutation {
   downloadLocale(
     locale: String!
-  ): DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
 
   updateLocale(
     locale: String!
     autoUpdate: Boolean!
     namespacing: Boolean!
     namespaces: [String]!
-  ): DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
 }
 
 # -----------------------------------------------
@@ -42,25 +25,18 @@ type LocalizationMutation {
 # -----------------------------------------------
 
 type LocalizationLocale {
-  availability: Int!
-  code: String!
-  createdAt: Date!
+  availability: Int
+  code: String
+  createdAt: Date
   installDate: Date
-  isInstalled: Boolean!
-  isRTL: Boolean!
-  name: String!
-  nativeName: String!
-  updatedAt: Date!
-}
-
-type LocalizationConfig {
-  locale: String!
-  autoUpdate: Boolean!
-  namespacing: Boolean!
-  namespaces: [String]!
+  isInstalled: Boolean
+  isRTL: Boolean
+  name: String
+  nativeName: String
+  updatedAt: Date
 }
 
 type Translation {
-  key: String!
-  value: String!
+  key: String
+  value: String
 }

+ 0 - 64
server/graph/schemas/logging.graphql

@@ -1,64 +0,0 @@
-# ===============================================
-# LOGGING
-# ===============================================
-
-extend type Query {
-  logging: LoggingQuery
-}
-
-extend type Mutation {
-  logging: LoggingMutation
-}
-
-extend type Subscription {
-  loggingLiveTrail: LoggerTrailLine
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
-
-type LoggingQuery {
-  loggers(
-    filter: String
-    orderBy: String
-  ): [Logger] @auth(requires: ["manage:system"])
-}
-
-# -----------------------------------------------
-# MUTATIONS
-# -----------------------------------------------
-
-type LoggingMutation {
-  updateLoggers(
-    loggers: [LoggerInput]
-  ): DefaultResponse @auth(requires: ["manage:system"])
-}
-
-# -----------------------------------------------
-# TYPES
-# -----------------------------------------------
-
-type Logger {
-  isEnabled: Boolean!
-  key: String!
-  title: String!
-  description: String
-  logo: String
-  website: String
-  level: String
-  config: [KeyValuePair]
-}
-
-input LoggerInput {
-  isEnabled: Boolean!
-  key: String!
-  level: String!
-  config: [KeyValuePairInput]
-}
-
-type LoggerTrailLine {
-  level: String!
-  output: String!
-  timestamp: Date!
-}

+ 17 - 33
server/graph/schemas/mail.graphql

@@ -3,31 +3,15 @@
 # ===============================================
 
 extend type Query {
-  mail: MailQuery
+  mailConfig: MailConfig
 }
 
 extend type Mutation {
-  mail: MailMutation
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
-
-type MailQuery {
-  config: MailConfig @auth(requires: ["manage:system"])
-}
-
-# -----------------------------------------------
-# MUTATIONS
-# -----------------------------------------------
-
-type MailMutation {
-  sendTest(
+  sendMailTest(
     recipientEmail: String!
-  ): DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
 
-  updateConfig(
+  updateMailConfig(
     senderName: String!
     senderEmail: String!
     host: String!
@@ -40,7 +24,7 @@ type MailMutation {
     dkimDomainName: String!
     dkimKeySelector: String!
     dkimPrivateKey: String!
-  ): DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
 }
 
 # -----------------------------------------------
@@ -48,16 +32,16 @@ type MailMutation {
 # -----------------------------------------------
 
 type MailConfig {
-  senderName: String!
-  senderEmail: String!
-  host: String!
-  port: Int!
-  secure: Boolean!
-  verifySSL: Boolean!
-  user: String!
-  pass: String!
-  useDKIM: Boolean!
-  dkimDomainName: String!
-  dkimKeySelector: String!
-  dkimPrivateKey: String!
+  senderName: String
+  senderEmail: String
+  host: String
+  port: Int
+  secure: Boolean
+  verifySSL: Boolean
+  user: String
+  pass: String
+  useDKIM: Boolean
+  dkimDomainName: String
+  dkimKeySelector: String
+  dkimPrivateKey: String
 }

+ 12 - 24
server/graph/schemas/navigation.graphql

@@ -3,33 +3,21 @@
 # ===============================================
 
 extend type Query {
-  navigation: NavigationQuery
-}
-
-extend type Mutation {
-  navigation: NavigationMutation
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
-
-type NavigationQuery {
-  tree: [NavigationTree]! @auth(requires: ["manage:navigation", "manage:system"])
-  config: NavigationConfig! @auth(requires: ["manage:navigation", "manage:system"])
+  navigationTree: [NavigationTree]
+  navigationConfig: NavigationConfig
 }
 
 # -----------------------------------------------
 # MUTATIONS
 # -----------------------------------------------
 
-type NavigationMutation {
-  updateTree(
+extend type Mutation {
+  updateNavigationTree(
     tree: [NavigationTreeInput]!
-  ): DefaultResponse @auth(requires: ["manage:navigation", "manage:system"])
-  updateConfig(
+  ): DefaultResponse
+  updateNavigationConfig(
     mode: NavigationMode!
-  ): DefaultResponse @auth(requires: ["manage:navigation", "manage:system"])
+  ): DefaultResponse
 }
 
 # -----------------------------------------------
@@ -37,8 +25,8 @@ type NavigationMutation {
 # -----------------------------------------------
 
 type NavigationTree {
-  locale: String!
-  items: [NavigationItem]!
+  locale: String
+  items: [NavigationItem]
 }
 
 input NavigationTreeInput {
@@ -47,8 +35,8 @@ input NavigationTreeInput {
 }
 
 type NavigationItem {
-  id: String!
-  kind: String!
+  id: String
+  kind: String
   label: String
   icon: String
   targetType: String
@@ -69,7 +57,7 @@ input NavigationItemInput {
 }
 
 type NavigationConfig {
-  mode: NavigationMode!
+  mode: NavigationMode
 }
 
 enum NavigationMode {

+ 133 - 149
server/graph/schemas/page.graphql

@@ -3,36 +3,24 @@
 # ===============================================
 
 extend type Query {
-  pages: PageQuery
-}
-
-extend type Mutation {
-  pages: PageMutation
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
-
-type PageQuery {
-  history(
+  pageHistoryById(
     id: Int!
     offsetPage: Int
     offsetSize: Int
-  ): PageHistoryResult @auth(requires: ["manage:system", "read:history"])
+  ): PageHistoryResult
 
-  version(
+  pageVersionById(
     pageId: Int!
     versionId: Int!
-  ): PageVersion @auth(requires: ["manage:system", "read:history"])
+  ): PageVersion
 
-  search(
+  searchPages(
     query: String!
     path: String
     locale: String
-  ): PageSearchResponse! @auth(requires: ["manage:system", "read:pages"])
+  ): PageSearchResponse!
 
-  list(
+  pages(
     limit: Int
     orderBy: PageOrderBy
     orderByDirection: PageOrderByDirection
@@ -40,46 +28,42 @@ type PageQuery {
     locale: String
     creatorId: Int
     authorId: Int
-  ): [PageListItem!]! @auth(requires: ["manage:system", "read:pages"])
+  ): [PageListItem!]!
 
-  single(
+  pageById(
     id: Int!
-  ): Page @auth(requires: ["read:pages", "manage:system"])
+  ): Page
 
-  tags: [PageTag]! @auth(requires: ["manage:system", "read:pages"])
+  tags: [PageTag]!
 
   searchTags(
     query: String!
-  ): [String]! @auth(requires: ["manage:system", "read:pages"])
+  ): [String]!
 
-  tree(
+  pageTree(
     path: String
     parent: Int
     mode: PageTreeMode!
     locale: String!
     includeAncestors: Boolean
-  ): [PageTreeItem] @auth(requires: ["manage:system", "read:pages"])
+  ): [PageTreeItem]
 
-  links(
+  pageLinks(
     locale: String!
-  ): [PageLinkItem] @auth(requires: ["manage:system", "read:pages"])
+  ): [PageLinkItem]
 
   checkConflicts(
     id: Int!
     checkoutDate: Date!
-  ): Boolean! @auth(requires: ["write:pages", "manage:pages", "manage:system"])
+  ): Boolean!
 
-  conflictLatest(
+  checkConflictsLatest(
     id: Int!
-  ): PageConflictLatest! @auth(requires: ["write:pages", "manage:pages", "manage:system"])
+  ): PageConflictLatest!
 }
 
-# -----------------------------------------------
-# MUTATIONS
-# -----------------------------------------------
-
-type PageMutation {
-  create(
+extend type Mutation {
+  createPage(
     content: String!
     description: String!
     editor: String!
@@ -93,9 +77,9 @@ type PageMutation {
     scriptJs: String
     tags: [String]!
     title: String!
-  ): PageResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"])
+  ): PageResponse
 
-  update(
+  updatePage(
     id: Int!
     content: String
     description: String
@@ -110,54 +94,54 @@ type PageMutation {
     scriptJs: String
     tags: [String]
     title: String
-  ): PageResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"])
+  ): PageResponse
 
-  convert(
+  convertPage(
     id: Int!
     editor: String!
-  ): DefaultResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"])
+  ): DefaultResponse
 
-  move(
+  renamePage(
     id: Int!
     destinationPath: String!
     destinationLocale: String!
-  ): DefaultResponse @auth(requires: ["manage:pages", "manage:system"])
+  ): DefaultResponse
 
-  delete(
+  deletePage(
     id: Int!
-  ): DefaultResponse @auth(requires: ["delete:pages", "manage:system"])
+  ): DefaultResponse
 
   deleteTag(
     id: Int!
-  ): DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
 
   updateTag(
     id: Int!
     tag: String!
     title: String!
-  ): DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
 
-  flushCache: DefaultResponse @auth(requires: ["manage:system"])
+  flushCache: DefaultResponse
 
   migrateToLocale(
     sourceLocale: String!
     targetLocale: String!
-  ): PageMigrationResponse @auth(requires: ["manage:system"])
+  ): PageMigrationResponse
 
-  rebuildTree: DefaultResponse @auth(requires: ["manage:system"])
+  rebuildPageTree: DefaultResponse
 
-  render(
+  renderPage(
     id: Int!
-  ): DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
 
-  restore(
+  restorePage(
     pageId: Int!
     versionId: Int!
-  ): DefaultResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"])
+  ): DefaultResponse
 
-  purgeHistory (
+  purgePagesHistory (
     olderThan: String!
-  ): DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
 }
 
 # -----------------------------------------------
@@ -165,152 +149,152 @@ type PageMutation {
 # -----------------------------------------------
 
 type PageResponse {
-  responseResult: ResponseStatus!
+  operation: Operation
   page: Page
 }
 
 type PageMigrationResponse {
-  responseResult: ResponseStatus!
+  operation: Operation
   count: Int
 }
 
 type Page {
-  id: Int!
-  path: String!
-  hash: String!
-  title: String!
-  description: String!
-  isPrivate: Boolean! @auth(requires: ["write:pages", "manage:system"])
-  isPublished: Boolean! @auth(requires: ["write:pages", "manage:system"])
-  privateNS: String @auth(requires: ["write:pages", "manage:system"])
-  publishStartDate: Date! @auth(requires: ["write:pages", "manage:system"])
-  publishEndDate: Date! @auth(requires: ["write:pages", "manage:system"])
-  tags: [PageTag]!
-  content: String! @auth(requires: ["read:source", "write:pages", "manage:system"])
+  id: Int
+  path: String
+  hash: String
+  title: String
+  description: String
+  isPrivate: Boolean
+  isPublished: Boolean
+  privateNS: String
+  publishStartDate: Date
+  publishEndDate: Date
+  tags: [PageTag]
+  content: String
   render: String
   toc: String
-  contentType: String!
-  createdAt: Date!
-  updatedAt: Date!
-  editor: String! @auth(requires: ["write:pages", "manage:system"])
-  locale: String!
+  contentType: String
+  createdAt: Date
+  updatedAt: Date
+  editor: String
+  locale: String
   scriptCss: String
   scriptJs: String
-  authorId: Int! @auth(requires: ["write:pages", "manage:system"])
-  authorName: String! @auth(requires: ["write:pages", "manage:system"])
-  authorEmail: String! @auth(requires: ["write:pages", "manage:system"])
-  creatorId: Int! @auth(requires: ["write:pages", "manage:system"])
-  creatorName: String! @auth(requires: ["write:pages", "manage:system"])
-  creatorEmail: String! @auth(requires: ["write:pages", "manage:system"])
+  authorId: Int
+  authorName: String
+  authorEmail: String
+  creatorId: Int
+  creatorName: String
+  creatorEmail: String
 }
 
 type PageTag {
-  id: Int!
-  tag: String!
+  id: Int
+  tag: String
   title: String
-  createdAt: Date!
-  updatedAt: Date!
+  createdAt: Date
+  updatedAt: Date
 }
 
 type PageHistory {
-  versionId: Int!
-  versionDate: Date!
-  authorId: Int!
-  authorName: String!
-  actionType: String!
+  versionId: Int
+  versionDate: Date
+  authorId: Int
+  authorName: String
+  actionType: String
   valueBefore: String
   valueAfter: String
 }
 
 type PageVersion {
-  action: String!
-  authorId: String!
-  authorName: String!
-  content: String!
-  contentType: String!
-  createdAt: Date!
-  versionDate: Date!
-  description: String!
-  editor: String!
-  isPrivate: Boolean!
-  isPublished: Boolean!
-  locale: String!
-  pageId: Int!
-  path: String!
-  publishEndDate: Date!
-  publishStartDate: Date!
-  tags: [String]!
-  title: String!
-  versionId: Int!
+  action: String
+  authorId: String
+  authorName: String
+  content: String
+  contentType: String
+  createdAt: Date
+  versionDate: Date
+  description: String
+  editor: String
+  isPrivate: Boolean
+  isPublished: Boolean
+  locale: String
+  pageId: Int
+  path: String
+  publishEndDate: Date
+  publishStartDate: Date
+  tags: [String]
+  title: String
+  versionId: Int
 }
 
 type PageHistoryResult {
   trail: [PageHistory]
-  total: Int!
+  total: Int
 }
 
 type PageSearchResponse {
-  results: [PageSearchResult]!
-  suggestions: [String]!
-  totalHits: Int!
+  results: [PageSearchResult]
+  suggestions: [String]
+  totalHits: Int
 }
 
 type PageSearchResult {
-  id: String!
-  title: String!
-  description: String!
-  path: String!
-  locale: String!
+  id: String
+  title: String
+  description: String
+  path: String
+  locale: String
 }
 
 type PageListItem {
-  id: Int!
-  path: String!
-  locale: String!
+  id: Int
+  path: String
+  locale: String
   title: String
   description: String
-  contentType: String!
-  isPublished: Boolean!
-  isPrivate: Boolean!
+  contentType: String
+  isPublished: Boolean
+  isPrivate: Boolean
   privateNS: String
-  createdAt: Date!
-  updatedAt: Date!
+  createdAt: Date
+  updatedAt: Date
   tags: [String]
 }
 
 type PageTreeItem {
-  id: Int!
-  path: String!
-  depth: Int!
-  title: String!
-  isPrivate: Boolean!
-  isFolder: Boolean!
+  id: Int
+  path: String
+  depth: Int
+  title: String
+  isPrivate: Boolean
+  isFolder: Boolean
   privateNS: String
   parent: Int
   pageId: Int
-  locale: String!
+  locale: String
 }
 
 type PageLinkItem {
-  id: Int!
-  path: String!
-  title: String!
-  links: [String]!
+  id: Int
+  path: String
+  title: String
+  links: [String]
 }
 
 type PageConflictLatest {
-  id: Int!
-  authorId: String!
-  authorName: String!
-  content: String!
-  createdAt: Date!
-  description: String!
-  isPublished: Boolean!
-  locale: String!
-  path: String!
+  id: Int
+  authorId: String
+  authorName: String
+  content: String
+  createdAt: Date
+  description: String
+  isPublished: Boolean
+  locale: String
+  path: String
   tags: [String]
-  title: String!
-  updatedAt: Date!
+  title: String
+  updatedAt: Date
 }
 
 enum PageOrderBy {

+ 7 - 23
server/graph/schemas/rendering.graphql

@@ -3,32 +3,16 @@
 # ===============================================
 
 extend type Query {
-  rendering: RenderingQuery
-}
-
-extend type Mutation {
-  rendering: RenderingMutation
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
-
-type RenderingQuery {
   renderers(
     filter: String
     orderBy: String
-  ): [Renderer] @auth(requires: ["manage:system"])
+  ): [Renderer]
 }
 
-# -----------------------------------------------
-# MUTATIONS
-# -----------------------------------------------
-
-type RenderingMutation {
+extend type Mutation {
   updateRenderers(
-    renderers: [RendererInput]
-  ): DefaultResponse @auth(requires: ["manage:system"])
+    renderers: [RendererInput]!
+  ): DefaultResponse
 }
 
 # -----------------------------------------------
@@ -36,9 +20,9 @@ type RenderingMutation {
 # -----------------------------------------------
 
 type Renderer {
-  isEnabled: Boolean!
-  key: String!
-  title: String!
+  isEnabled: Boolean
+  key: String
+  title: String
   description: String
   icon: String
   dependsOn: String

+ 1 - 45
server/graph/schemas/search.graphql

@@ -2,54 +2,10 @@
 # SEARCH
 # ===============================================
 
-extend type Query {
-  search: SearchQuery
-}
-
 extend type Mutation {
-  search: SearchMutation
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
-
-type SearchQuery {
-  searchEngines(
-    filter: String
-    orderBy: String
-  ): [SearchEngine] @auth(requires: ["manage:system"])
-}
-
-# -----------------------------------------------
-# MUTATIONS
-# -----------------------------------------------
-
-type SearchMutation {
-  updateSearchEngines(
-    engines: [SearchEngineInput]
-  ): DefaultResponse @auth(requires: ["manage:system"])
-
-  rebuildIndex: DefaultResponse @auth(requires: ["manage:system"])
+  rebuildSearchIndex: DefaultResponse
 }
 
 # -----------------------------------------------
 # TYPES
 # -----------------------------------------------
-
-type SearchEngine {
-  isEnabled: Boolean!
-  key: String!
-  title: String!
-  description: String
-  logo: String
-  website: String
-  isAvailable: Boolean
-  config: [KeyValuePair]
-}
-
-input SearchEngineInput {
-  isEnabled: Boolean!
-  key: String!
-  config: [KeyValuePairInput]
-}

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

@@ -13,9 +13,6 @@ extend type Query {
     hostname: String!
     exact: Boolean!
   ): Site @auth(requires: ["manage:system"])
-
-  # Legacy
-  site: SiteQuery
 }
 
 extend type Mutation {
@@ -42,59 +39,6 @@ extend type Mutation {
   deleteSite (
     id: UUID!
   ): DefaultResponse @auth(requires: ["manage:system"])
-
-  # Legacy
-  site: SiteMutation
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
-
-type SiteQuery {
-  config: SiteConfig @auth(requires: ["manage:system"])
-}
-
-# -----------------------------------------------
-# MUTATIONS
-# -----------------------------------------------
-
-type SiteMutation {
-  updateConfig(
-    host: String
-    title: String
-    description: String
-    robots: [String]
-    analyticsService: String
-    analyticsId: String
-    company: String
-    contentLicense: String
-    logoUrl: String
-    authAutoLogin: Boolean
-    authEnforce2FA: Boolean
-    authHideLocal: Boolean
-    authLoginBgUrl: String
-    authJwtAudience: String
-    authJwtExpiration: String
-    authJwtRenewablePeriod: String
-    featurePageRatings: Boolean
-    featurePageComments: Boolean
-    featurePersonalWikis: Boolean
-    securityOpenRedirect: Boolean
-    securityIframe: Boolean
-    securityReferrerPolicy: Boolean
-    securityTrustProxy: Boolean
-    securitySRI: Boolean
-    securityHSTS: Boolean
-    securityHSTSDuration: Int
-    securityCSP: Boolean
-    securityCSPDirectives: String
-    uploadMaxFileSize: Int
-    uploadMaxFiles: Int
-    uploadScanSVG: Boolean
-    uploadForceDownload: Boolean
-
-  ): DefaultResponse @auth(requires: ["manage:system"])
 }
 
 # -----------------------------------------------
@@ -174,7 +118,7 @@ enum SitePageRatingModes {
 }
 
 type SiteCreateResponse {
-  status: ResponseStatus
+  operation: Operation
   site: Site
 }
 
@@ -227,40 +171,3 @@ input SiteThemeInput {
   showSharingMenu: Boolean
   showPrintBtn: Boolean
 }
-
-# LEGACY
-
-type SiteConfig {
-  host: String
-  title: String
-  description: String
-  robots: [String]
-  analyticsService: String
-  analyticsId: String
-  company: String
-  contentLicense: String
-  logoUrl: String
-  authAutoLogin: Boolean
-  authEnforce2FA: Boolean
-  authHideLocal: Boolean
-  authLoginBgUrl: String
-  authJwtAudience: String
-  authJwtExpiration: String
-  authJwtRenewablePeriod: String
-  featurePageRatings: Boolean
-  featurePageComments: Boolean
-  featurePersonalWikis: Boolean
-  securityOpenRedirect: Boolean
-  securityIframe: Boolean
-  securityReferrerPolicy: Boolean
-  securityTrustProxy: Boolean
-  securitySRI: Boolean
-  securityHSTS: Boolean
-  securityHSTSDuration: Int
-  securityCSP: Boolean
-  securityCSPDirectives: String
-  uploadMaxFileSize: Int
-  uploadMaxFiles: Int
-  uploadScanSVG: Boolean
-  uploadForceDownload: Boolean
-}

+ 51 - 50
server/graph/schemas/storage.graphql

@@ -3,35 +3,30 @@
 # ===============================================
 
 extend type Query {
-  storage: StorageQuery
+  storageTargets(
+    siteId: UUID!
+    ): [StorageTarget]
 }
 
 extend type Mutation {
-  storage: StorageMutation
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
+  updateStorageTargets(
+    siteId: UUID!
+    targets: [StorageTargetInput]!
+  ): DefaultResponse
 
-type StorageQuery {
-  targets: [StorageTarget] @auth(requires: ["manage:system"])
-  status: [StorageStatus] @auth(requires: ["manage:system"])
-}
+  setupStorageTarget(
+    targetId: UUID!
+    state: JSON!
+  ): StorageTargetSetupResponse
 
-# -----------------------------------------------
-# MUTATIONS
-# -----------------------------------------------
+  destroyStorageTargetSetup(
+    targetId: UUID!
+  ): DefaultResponse
 
-type StorageMutation {
-  updateTargets(
-    targets: [StorageTargetInput]!
-  ): DefaultResponse @auth(requires: ["manage:system"])
-
-  executeAction(
-    targetKey: String!
+  executeStorageAction(
+    targetId: UUID!
     handler: String!
-  ): DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
 }
 
 # -----------------------------------------------
@@ -39,40 +34,46 @@ type StorageMutation {
 # -----------------------------------------------
 
 type StorageTarget {
-  isAvailable: Boolean!
-  isEnabled: Boolean!
-  key: String!
-  title: String!
+  id: UUID
+  isEnabled: Boolean
+  module: String
+  title: String
   description: String
-  logo: String
+  icon: String
+  banner: String
+  vendor: String
   website: String
-  supportedModes: [String]
-  mode: String
-  hasSchedule: Boolean!
-  syncInterval: String
-  syncIntervalDefault: String
-  config: [KeyValuePair]
-  actions: [StorageTargetAction]
+  contentTypes: JSON
+  assetDelivery: JSON
+  versioning: JSON
+  sync: JSON
+  status: JSON
+  setup: JSON
+  config: JSON
+  actions: JSON
 }
 
-input StorageTargetInput {
-  isEnabled: Boolean!
-  key: String!
-  mode: String!
-  syncInterval: String
-  config: [KeyValuePairInput]
+type StorageTargetSetupResponse {
+  operation: Operation
+  state: JSON
 }
 
-type StorageStatus {
-  key: String!
-  title: String!
-  status: String!
-  message: String!
-  lastAttempt: String!
+input StorageTargetInput {
+  id: UUID!
+  module: String!
+  isEnabled: Boolean
+  contentTypes: [String!]
+  largeThreshold: String
+  assetDeliveryFileStreaming: Boolean
+  assetDeliveryDirectAccess: Boolean
+  syncMode: StorageTargetSyncMode
+  syncInterval: String
+  useVersioning: Boolean
+  config: JSON
 }
 
-type StorageTargetAction {
-  handler: String!
-  label: String!
-  hint: String!
+enum StorageTargetSyncMode {
+  PULL
+  PUSH
+  SYNC
 }

+ 97 - 77
server/graph/schemas/system.graphql

@@ -3,50 +3,41 @@
 # ===============================================
 
 extend type Query {
-  system: SystemQuery
+  systemExtensions: [SystemExtension]
+  systemFlags: [SystemFlag]
+  systemInfo: SystemInfo
+  systemSecurity: SystemSecurity
 }
 
 extend type Mutation {
-  system: SystemMutation
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
-
-type SystemQuery {
-  flags: [SystemFlag] @auth(requires: ["manage:system"])
-  info: SystemInfo
-  extensions: [SystemExtension]! @auth(requires: ["manage:system"])
-}
-
-# -----------------------------------------------
-# MUTATIONS
-# -----------------------------------------------
-
-type SystemMutation {
-  updateFlags(
+  updateSystemFlags(
     flags: [SystemFlagInput]!
-  ): DefaultResponse @auth(requires: ["manage:system"])
-
-  resetTelemetryClientId: DefaultResponse @auth(requires: ["manage:system"])
-
-  setTelemetry(
-    enabled: Boolean!
-  ): DefaultResponse @auth(requires: ["manage:system"])
-
-  performUpgrade: DefaultResponse @auth(requires: ["manage:system"])
-
-  importUsersFromV1(
-    mongoDbConnString: String!
-    groupMode: SystemImportUsersGroupMode!
-  ): SystemImportUsersResponse @auth(requires:  ["manage:system"])
-
-  setHTTPSRedirection(
-    enabled: Boolean!
-  ): DefaultResponse @auth(requires: ["manage:system"])
-
-  renewHTTPSCertificate: DefaultResponse @auth(requires: ["manage:system"])
+  ): DefaultResponse
+
+  updateSystemSecurity(
+    authJwtAudience: String
+    authJwtExpiration: String
+    authJwtRenewablePeriod: String
+    corsConfig: String
+    corsMode: SystemSecurityCorsMode
+    cspDirectives: String
+    disallowFloc: Boolean
+    disallowIframe: Boolean
+    disallowOpenRedirect: Boolean
+    enforceCsp: Boolean
+    enforceHsts: Boolean
+    enforceSameOriginReferrerPolicy: Boolean
+    forceAssetDownload: Boolean
+    hstsDuration: Int
+    trustProxy: Boolean
+    uploadMaxFiles: Int
+    uploadMaxFileSize: Int
+    uploadScanSVG: Boolean
+  ): DefaultResponse
+
+  installExtension(
+    key: String!
+  ): DefaultResponse
 }
 
 # -----------------------------------------------
@@ -54,8 +45,8 @@ type SystemMutation {
 # -----------------------------------------------
 
 type SystemFlag {
-  key: String!
-  value: Boolean!
+  key: String
+  value: Boolean
 }
 
 input SystemFlagInput {
@@ -64,35 +55,35 @@ input SystemFlagInput {
 }
 
 type SystemInfo {
-  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"])
-  httpPort: Int @auth(requires: ["manage:system"])
-  httpRedirection: Boolean @auth(requires: ["manage:system"])
-  httpsPort: Int @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"])
-  sslDomain: String @auth(requires: ["manage:system"])
-  sslExpirationDate: Date @auth(requires: ["manage:system"])
-  sslProvider: String @auth(requires: ["manage:system"])
-  sslStatus: String @auth(requires: ["manage:system"])
-  sslSubscriberEmail: String @auth(requires: ["manage:system"])
-  tagsTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:pages", "delete:pages"])
-  telemetry: Boolean @auth(requires: ["manage:system"])
-  telemetryClientId: String @auth(requires: ["manage:system"])
-  upgradeCapable: Boolean @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"])
+  configFile: String
+  cpuCores: Int
+  currentVersion: String
+  dbHost: String
+  dbType: String
+  dbVersion: String
+  groupsTotal: Int
+  hostname: String
+  httpPort: Int
+  httpRedirection: Boolean
+  httpsPort: Int
+  latestVersion: String
+  latestVersionReleaseDate: Date
+  nodeVersion: String
+  operatingSystem: String
+  pagesTotal: Int
+  platform: String
+  ramTotal: String
+  sslDomain: String
+  sslExpirationDate: Date
+  sslProvider: String
+  sslStatus: String
+  sslSubscriberEmail: String
+  tagsTotal: Int
+  telemetry: Boolean
+  telemetryClientId: String
+  upgradeCapable: Boolean
+  usersTotal: Int
+  workingDirectory: String
 }
 
 enum SystemImportUsersGroupMode {
@@ -102,7 +93,7 @@ enum SystemImportUsersGroupMode {
 }
 
 type SystemImportUsersResponse {
-  responseResult: ResponseStatus
+  operation: Operation
   usersCount: Int
   groupsCount: Int
   failed: [SystemImportUsersResponseFailed]
@@ -115,9 +106,38 @@ type SystemImportUsersResponseFailed {
 }
 
 type SystemExtension {
-  key: String!
-  title: String!
-  description: String!
-  isInstalled: Boolean!
-  isCompatible: Boolean!
+  key: String
+  title: String
+  description: String
+  isInstalled: Boolean
+  isInstallable: Boolean
+  isCompatible: Boolean
+}
+
+type SystemSecurity {
+  authJwtAudience: String
+  authJwtExpiration: String
+  authJwtRenewablePeriod: String
+  corsConfig: String
+  corsMode: SystemSecurityCorsMode
+  cspDirectives: String
+  disallowFloc: Boolean
+  disallowIframe: Boolean
+  disallowOpenRedirect: Boolean
+  enforceCsp: Boolean
+  enforceHsts: Boolean
+  enforceSameOriginReferrerPolicy: Boolean
+  forceAssetDownload: Boolean
+  hstsDuration: Int
+  trustProxy: Boolean
+  uploadMaxFiles: Int
+  uploadMaxFileSize: Int
+  uploadScanSVG: Boolean
+}
+
+enum SystemSecurityCorsMode {
+  OFF
+  REFLECT
+  HOSTNAMES
+  REGEX
 }

+ 0 - 54
server/graph/schemas/theming.graphql

@@ -1,54 +0,0 @@
-# ===============================================
-# THEMES
-# ===============================================
-
-extend type Query {
-  theming: ThemingQuery
-}
-
-extend type Mutation {
-  theming: ThemingMutation
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
-
-type ThemingQuery {
-  themes: [ThemingTheme] @auth(requires: ["manage:theme", "manage:system"])
-  config: ThemingConfig @auth(requires: ["manage:theme", "manage:system"])
-}
-
-# -----------------------------------------------
-# MUTATIONS
-# -----------------------------------------------
-
-type ThemingMutation {
-  setConfig(
-    theme: String!
-    iconset: String!
-    darkMode: Boolean!
-    injectCSS: String
-    injectHead: String
-    injectBody: String
-  ): DefaultResponse @auth(requires: ["manage:theme", "manage:system"])
-}
-
-# -----------------------------------------------
-# TYPES
-# -----------------------------------------------
-
-type ThemingConfig {
-  theme: String!
-  iconset: String!
-  darkMode: Boolean!
-  injectCSS: String
-  injectHead: String
-  injectBody: String
-}
-
-type ThemingTheme {
-  key: String
-  title: String
-  author: String
-}

+ 101 - 114
server/graph/schemas/user.graphql

@@ -3,90 +3,65 @@
 # ===============================================
 
 extend type Query {
-  users: UserQuery
-}
-
-extend type Mutation {
-  users: UserMutation
-}
-
-# -----------------------------------------------
-# QUERIES
-# -----------------------------------------------
-
-type UserQuery {
-  list(
+  users (
+    page: Int
+    pageSize: Int
+    orderBy: UserOrderBy
+    orderByDirection: OrderByDirection
+    # Filter by name / email
     filter: String
-    orderBy: String
-  ): [UserMinimal] @auth(requires: ["write:users", "manage:users", "manage:system"])
-
-  search(
-    query: String!
-  ): [UserMinimal] @auth(requires: ["write:groups", "manage:groups", "write:users", "manage:users", "manage:system"])
+  ): [UserMinimal]
 
-  single(
-    id: Int!
-  ): User @auth(requires: ["manage:users", "manage:system"])
+  userById(
+    id: UUID!
+  ): User
 
   profile: UserProfile
 
-  lastLogins: [UserLastLogin] @auth(requires: ["write:groups", "manage:groups", "write:users", "manage:users", "manage:system"])
+  lastLogins: [UserLastLogin]
 }
 
-# -----------------------------------------------
-# MUTATIONS
-# -----------------------------------------------
-
-type UserMutation {
-  create(
+extend type Mutation {
+  createUser(
     email: String!
     name: String!
-    passwordRaw: String
-    providerKey: String!
-    groups: [Int]!
-    mustChangePassword: Boolean
-    sendWelcomeEmail: Boolean
-  ): UserResponse @auth(requires: ["write:users", "manage:users", "manage:system"])
-
-  update(
-    id: Int!
-    email: String
-    name: String
-    newPassword: String
-    groups: [Int]
-    location: String
-    jobTitle: String
-    timezone: String
-    dateFormat: String
-    appearance: String
-  ): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
-
-  delete(
-    id: Int!
-    replaceId: Int!
-  ): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
+    password: String!
+    groups: [UUID]!
+    mustChangePassword: Boolean!
+    sendWelcomeEmail: Boolean!
+  ): UserResponse
+
+  updateUser(
+    id: UUID!
+    patch: UserUpdateInput!
+  ): DefaultResponse
 
-  verify(
-    id: Int!
-  ): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
+  deleteUser(
+    id: UUID!
+    replaceId: UUID!
+  ): DefaultResponse
 
-  activate(
-    id: Int!
-  ): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
+  verifyUser(
+    id: UUID!
+  ): DefaultResponse
 
-  deactivate(
-    id: Int!
-  ): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
+  activateUser(
+    id: UUID!
+  ): DefaultResponse
 
-  enableTFA(
-    id: Int!
-  ): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
+  deactivateUser(
+    id: UUID!
+  ): DefaultResponse
 
-  disableTFA(
-    id: Int!
-  ): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
+  enableUserTFA(
+    id: UUID!
+  ): DefaultResponse
 
-  resetPassword(
+  disableUserTFA(
+    id: UUID!
+  ): DefaultResponse
+
+  resetUserPassword(
     id: Int!
   ): DefaultResponse
 
@@ -110,71 +85,83 @@ type UserMutation {
 # -----------------------------------------------
 
 type UserResponse {
-  responseResult: ResponseStatus!
+  operation: Operation
   user: User
 }
 
 type UserLastLogin {
-  id: Int!
-  name: String!
-  lastLoginAt: Date!
+  id: UUID
+  name: String
+  lastLoginAt: Date
 }
 
 type UserMinimal {
-  id: Int!
-  name: String!
-  email: String!
-  providerKey: String!
-  isSystem: Boolean!
-  isActive: Boolean!
-  createdAt: Date!
+  id: UUID
+  name: String
+  email: String
+  isSystem: Boolean
+  isActive: Boolean
+  createdAt: Date
   lastLoginAt: Date
 }
 
 type User {
-  id: Int!
-  name: String!
-  email: String!
-  providerKey: String!
-  providerName: String
-  providerId: String
-  providerIs2FACapable: Boolean
-  isSystem: Boolean!
-  isActive: Boolean!
-  isVerified: Boolean!
-  location: String!
-  jobTitle: String!
-  timezone: String!
-  dateFormat: String!
-  appearance: String!
-  createdAt: Date!
-  updatedAt: Date!
+  id: UUID
+  name: String
+  email: String
+  auth: JSON
+  isSystem: Boolean
+  isActive: Boolean
+  isVerified: Boolean
+  meta: JSON
+  prefs: JSON
+  createdAt: Date
+  updatedAt: Date
   lastLoginAt: Date
-  tfaIsActive: Boolean!
-  groups: [Group]!
+  groups: [Group]
 }
 
 type UserProfile {
-  id: Int!
-  name: String!
-  email: String!
+  id: Int
+  name: String
+  email: String
   providerKey: String
   providerName: String
-  isSystem: Boolean!
-  isVerified: Boolean!
-  location: String!
-  jobTitle: String!
-  timezone: String!
-  dateFormat: String!
-  appearance: String!
-  createdAt: Date!
-  updatedAt: Date!
+  isSystem: Boolean
+  isVerified: Boolean
+  location: String
+  jobTitle: String
+  timezone: String
+  dateFormat: String
+  appearance: String
+  createdAt: Date
+  updatedAt: Date
   lastLoginAt: Date
-  groups: [String]!
-  pagesTotal: Int!
+  groups: [String]
+  pagesTotal: Int
 }
 
 type UserTokenResponse {
-  responseResult: ResponseStatus!
+  operation: Operation
   jwt: String
 }
+
+enum UserOrderBy {
+  id
+  email
+  name
+  createdAt
+  updatedAt
+  lastLoginAt
+}
+
+input UserUpdateInput {
+  email: String
+  name: String
+  newPassword: String
+  groups: [UUID!]
+  isActive: Boolean
+  isVerified: Boolean
+  meta: JSON
+  prefs: JSON
+}

+ 102 - 81
ux/src/components/SiteActivateDialog.vue

@@ -1,114 +1,135 @@
 <template lang="pug">
-q-dialog(ref='dialog', @hide='onDialogHide')
+q-dialog(ref='dialogRef', @hide='onDialogHide')
   q-card(style='min-width: 350px; max-width: 450px;')
     q-card-section.card-header
       q-icon(name='img:/_assets/icons/fluent-shutdown.svg', left, size='sm')
-      span {{value ? $t(`admin.sites.activate`) : $t(`admin.sites.deactivate`)}}
+      span {{modelValue ? t(`admin.sites.activate`) : t(`admin.sites.deactivate`)}}
     q-card-section
       .text-body2
-        i18n-t(:keypath='value ? `admin.sites.activateConfirm` : `admin.sites.deactivateConfirm`')
+        i18n-t(:keypath='modelValue ? `admin.sites.activateConfirm` : `admin.sites.deactivateConfirm`')
           template(v-slot:siteTitle)
-            strong {{site.title}}
+            strong {{props.site.title}}
     q-card-actions.card-actions
       q-space
       q-btn.acrylic-btn(
         flat
-        :label='$t(`common.actions.cancel`)'
+        :label='t(`common.actions.cancel`)'
         color='grey'
         padding='xs md'
-        @click='hide'
+        @click='onDialogCancel'
         )
       q-btn(
         unelevated
-        :label='value ? $t(`common.actions.activate`) : $t(`common.actions.deactivate`)'
-        :color='value ? `positive` : `negative`'
+        :label='modelValue ? t(`common.actions.activate`) : t(`common.actions.deactivate`)'
+        :color='modelValue ? `positive` : `negative`'
         padding='xs md'
         @click='confirm'
+        :loading='state.isLoading'
         )
 </template>
 
-<script>
+<script setup>
 import gql from 'graphql-tag'
 import cloneDeep from 'lodash/cloneDeep'
+import { useI18n } from 'vue-i18n'
+import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { reactive, ref } from 'vue'
 
-export default {
-  props: {
-    site: {
-      type: Object
-    },
-    value: {
-      type: Boolean,
-      default: false
-    }
-  },
-  emits: ['ok', 'hide'],
-  data () {
-    return {
-    }
+import { useAdminStore } from '../stores/admin'
+
+// PROPS
+
+const props = defineProps({
+  site: {
+    type: Object,
+    required: true
   },
-  methods: {
-    show () {
-      this.$refs.dialog.show()
-    },
-    hide () {
-      this.$refs.dialog.hide()
-    },
-    onDialogHide () {
-      this.$emit('hide')
-    },
-    async confirm () {
-      try {
-        const siteId = this.site.id
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation updateSite (
-              $id: UUID!
-              $newState: Boolean
-              ) {
-              updateSite(
-                id: $id
-                patch: {
-                  isEnabled: $newState
-                }
-                ) {
-                status {
-                  succeeded
-                  message
-                }
-              }
+  modelValue: {
+    type: Boolean,
+    default: false
+  }
+})
+
+// EMITS
+
+defineEmits([
+  ...useDialogPluginComponent.emits
+])
+
+// QUASAR
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
+const $q = useQuasar()
+
+// STORES
+
+const adminStore = useAdminStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// DATA
+
+const state = reactive({
+  isLoading: false
+})
+
+// METHODS
+
+async function confirm () {
+  state.isLoading = true
+  try {
+    const resp = await APOLLO_CLIENT.mutate({
+      mutation: gql`
+        mutation updateSite (
+          $id: UUID!
+          $newState: Boolean
+          ) {
+          updateSite(
+            id: $id
+            patch: {
+              isEnabled: $newState
             }
-          `,
-          variables: {
-            id: siteId,
-            newState: this.value
-          }
-        })
-        if (resp?.data?.updateSite?.status?.succeeded) {
-          this.$q.notify({
-            type: 'positive',
-            message: this.$t('admin.sites.updateSuccess')
-          })
-          this.$store.set('admin/sites', this.$store.get('admin/sites').map(s => {
-            if (s.id === siteId) {
-              const ns = cloneDeep(s)
-              ns.isEnabled = this.value
-              return ns
-            } else {
-              return s
+            ) {
+            operation {
+              succeeded
+              message
             }
-          }))
-          this.$emit('ok')
-          this.hide()
-        } else {
-          throw new Error(resp?.data?.updateSite?.status?.message || 'An unexpected error occured.')
+          }
         }
-      } catch (err) {
-        this.$q.notify({
-          type: 'negative',
-          message: err.message
-        })
+      `,
+      variables: {
+        id: props.site.id,
+        newState: props.modelValue
       }
+    })
+    if (resp?.data?.updateSite?.operation?.succeeded) {
+      $q.notify({
+        type: 'positive',
+        message: this.$t('admin.sites.updateSuccess')
+      })
+      adminStore.$patch({
+        sites: adminStore.sites.map(s => {
+          if (s.id === props.site.id) {
+            const ns = cloneDeep(s)
+            ns.isEnabled = props.modelValue
+            return ns
+          } else {
+            return s
+          }
+        })
+      })
+      onDialogOK()
+    } else {
+      throw new Error(resp?.data?.updateSite?.operation?.message || 'An unexpected error occured.')
     }
+  } catch (err) {
+    $q.notify({
+      type: 'negative',
+      message: err.message
+    })
   }
+  state.isLoading = false
 }
 </script>

+ 5 - 3
ux/src/components/SiteCreateDialog.vue

@@ -66,6 +66,8 @@ import { reactive, ref } from 'vue'
 
 import { useAdminStore } from '../stores/admin'
 
+// EMITS
+
 defineEmits([
   ...useDialogPluginComponent.emits
 ])
@@ -114,7 +116,7 @@ async function create () {
             hostname: $hostname
             title: $title
             ) {
-            status {
+            operation {
               succeeded
               message
             }
@@ -126,7 +128,7 @@ async function create () {
         title: state.siteName
       }
     })
-    if (resp?.data?.createSite?.status?.succeeded) {
+    if (resp?.data?.createSite?.operation?.succeeded) {
       $q.notify({
         type: 'positive',
         message: t('admin.sites.createSuccess')
@@ -134,7 +136,7 @@ async function create () {
       await adminStore.fetchSites()
       onDialogOK()
     } else {
-      throw new Error(resp?.data?.createSite?.status?.message || 'An unexpected error occured.')
+      throw new Error(resp?.data?.createSite?.operation?.message || 'An unexpected error occured.')
     }
   } catch (err) {
     $q.notify({

+ 81 - 60
ux/src/components/SiteDeleteDialog.vue

@@ -1,94 +1,115 @@
 <template lang="pug">
-q-dialog(ref='dialog', @hide='onDialogHide')
+q-dialog(ref='dialogRef', @hide='onDialogHide')
   q-card(style='min-width: 350px; max-width: 450px;')
     q-card-section.card-header
       q-icon(name='img:/_assets/icons/fluent-delete-bin.svg', left, size='sm')
-      span {{$t(`admin.sites.delete`)}}
+      span {{t(`admin.sites.delete`)}}
     q-card-section
       .text-body2
         i18n-t(keypath='admin.sites.deleteConfirm')
           template(v-slot:siteTitle)
-            strong {{site.title}}
+            strong {{props.site.title}}
       .text-body2.q-mt-md
-        strong.text-negative {{$t(`admin.sites.deleteConfirmWarn`)}}
+        strong.text-negative {{t(`admin.sites.deleteConfirmWarn`)}}
     q-card-actions.card-actions
       q-space
       q-btn.acrylic-btn(
         flat
-        :label='$t(`common.actions.cancel`)'
+        :label='t(`common.actions.cancel`)'
         color='grey'
         padding='xs md'
-        @click='hide'
+        @click='onDialogCancel'
         )
       q-btn(
         unelevated
-        :label='$t(`common.actions.delete`)'
+        :label='t(`common.actions.delete`)'
         color='negative'
         padding='xs md'
         @click='confirm'
+        :loading='state.isLoading'
         )
 </template>
 
-<script>
+<script setup>
 import gql from 'graphql-tag'
+import { useI18n } from 'vue-i18n'
+import { useDialogPluginComponent, useQuasar } from 'quasar'
+import { reactive } from 'vue'
 
-export default {
-  props: {
-    site: {
-      type: Object
-    }
-  },
-  emits: ['ok', 'hide'],
-  data () {
-    return {
-    }
-  },
-  methods: {
-    show () {
-      this.$refs.dialog.show()
-    },
-    hide () {
-      this.$refs.dialog.hide()
-    },
-    onDialogHide () {
-      this.$emit('hide')
-    },
-    async confirm () {
-      try {
-        const siteId = this.site.id
-        const resp = await this.$apollo.mutate({
-          mutation: gql`
-            mutation deleteSite ($id: UUID!) {
-              deleteSite(id: $id) {
-                status {
-                  succeeded
-                  message
-                }
-              }
+import { useAdminStore } from '../stores/admin'
+
+// PROPS
+
+const props = defineProps({
+  site: {
+    type: Object,
+    required: true
+  }
+})
+
+// EMITS
+
+defineEmits([
+  ...useDialogPluginComponent.emits
+])
+
+// QUASAR
+
+const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
+const $q = useQuasar()
+
+// STORES
+
+const adminStore = useAdminStore()
+
+// I18N
+
+const { t } = useI18n()
+
+// DATA
+
+const state = reactive({
+  isLoading: false
+})
+
+// METHODS
+
+async function confirm () {
+  state.isLoading = true
+  try {
+    const resp = await APOLLO_CLIENT.mutate({
+      mutation: gql`
+        mutation deleteSite ($id: UUID!) {
+          deleteSite(id: $id) {
+            status {
+              succeeded
+              message
             }
-          `,
-          variables: {
-            id: siteId
           }
-        })
-        if (resp?.data?.deleteSite?.status?.succeeded) {
-          this.$q.notify({
-            type: 'positive',
-            message: this.$t('admin.sites.deleteSuccess')
-          })
-          this.$store.set('admin/sites', this.$store.get('admin/sites').filter(s => s.id !== siteId))
-          this.$emit('ok')
-          this.hide()
-        } else {
-          throw new Error(resp?.data?.deleteSite?.status?.message || 'An unexpected error occured.')
         }
-      } catch (err) {
-        this.$q.notify({
-          type: 'negative',
-          message: err.message
-        })
+      `,
+      variables: {
+        id: props.site.id
       }
+    })
+    if (resp?.data?.deleteSite?.status?.succeeded) {
+      $q.notify({
+        type: 'positive',
+        message: t('admin.sites.deleteSuccess')
+      })
+      adminStore.$patch({
+        sites: adminStore.sites.filter(s => s.id !== props.site.id)
+      })
+      onDialogOK()
+    } else {
+      throw new Error(resp?.data?.deleteSite?.status?.message || 'An unexpected error occured.')
     }
+  } catch (err) {
+    $q.notify({
+      type: 'negative',
+      message: err.message
+    })
   }
+  state.isLoading = false
 }
 </script>

+ 0 - 4
ux/src/pages/AdminSites.vue

@@ -133,10 +133,6 @@ useMeta({
   title: t('admin.sites.title')
 })
 
-// DATA
-
-const loading = ref(false)
-
 // METHODS
 
 async function refresh () {