Переглянути джерело

fix: add permissions to resolvers

NGPixel 1 рік тому
батько
коміт
2a3e1400a7

+ 5 - 1
server/core/auth.mjs

@@ -105,6 +105,8 @@ export default {
    * @param {Express Next Callback} next
    * @param {Express Next Callback} next
    */
    */
   authenticate (req, res, next) {
   authenticate (req, res, next) {
+    req.isAuthenticated = false
+
     WIKI.auth.passport.authenticate('jwt', { session: false }, async (err, user, info) => {
     WIKI.auth.passport.authenticate('jwt', { session: false }, async (err, user, info) => {
       if (err) { return next() }
       if (err) { return next() }
       let mustRevalidate = false
       let mustRevalidate = false
@@ -170,6 +172,7 @@ export default {
           WIKI.auth.guest.cacheExpiration = DateTime.utc().plus({ minutes: 1 })
           WIKI.auth.guest.cacheExpiration = DateTime.utc().plus({ minutes: 1 })
         }
         }
         req.user = WIKI.auth.guest
         req.user = WIKI.auth.guest
+        req.isAuthenticated = false
         return next()
         return next()
       }
       }
 
 
@@ -203,6 +206,7 @@ export default {
       // JWT is valid
       // JWT is valid
       req.logIn(user, { session: false }, (errc) => {
       req.logIn(user, { session: false }, (errc) => {
         if (errc) { return next(errc) }
         if (errc) { return next(errc) }
+        req.isAuthenticated = true
         next()
         next()
       })
       })
     })(req, res, next)
     })(req, res, next)
@@ -223,7 +227,7 @@ export default {
       return true
       return true
     }
     }
 
 
-    // Check Global Permissions
+    // Check Permissions
     if (_.intersection(userPermissions, permissions).length < 1) {
     if (_.intersection(userPermissions, permissions).length < 1) {
       return false
       return false
     }
     }

+ 19 - 7
server/graph/resolvers/authentication.mjs

@@ -17,6 +17,10 @@ export default {
      * List of API Keys
      * List of API Keys
      */
      */
     async apiKeys (obj, args, context) {
     async apiKeys (obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['read:api', 'manage:api'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
       const keys = await WIKI.db.apiKeys.query().orderBy(['isRevoked', 'name'])
       const keys = await WIKI.db.apiKeys.query().orderBy(['isRevoked', 'name'])
       return keys.map(k => ({
       return keys.map(k => ({
         id: k.id,
         id: k.id,
@@ -31,7 +35,11 @@ export default {
     /**
     /**
      * Current API State
      * Current API State
      */
      */
-    apiState () {
+    apiState (obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['read:api', 'manage:api', 'read:dashboard'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
       return WIKI.config.api.isEnabled
       return WIKI.config.api.isEnabled
     },
     },
     /**
     /**
@@ -82,6 +90,10 @@ export default {
      */
      */
     async createApiKey (obj, args, context) {
     async createApiKey (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:api'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         const key = await WIKI.db.apiKeys.createNewKey(args)
         const key = await WIKI.db.apiKeys.createNewKey(args)
         await WIKI.auth.reloadApiKeys()
         await WIKI.auth.reloadApiKeys()
         WIKI.events.outbound.emit('reloadApiKeys')
         WIKI.events.outbound.emit('reloadApiKeys')
@@ -136,7 +148,7 @@ export default {
       try {
       try {
         const userId = context.req.user?.id
         const userId = context.req.user?.id
         if (!userId) {
         if (!userId) {
-          throw new Error('ERR_USER_NOT_AUTHENTICATED')
+          throw new Error('ERR_NOT_AUTHENTICATED')
         }
         }
 
 
         const usr = await WIKI.db.users.query().findById(userId)
         const usr = await WIKI.db.users.query().findById(userId)
@@ -182,7 +194,7 @@ export default {
       try {
       try {
         const userId = context.req.user?.id
         const userId = context.req.user?.id
         if (!userId) {
         if (!userId) {
-          throw new Error('ERR_USER_NOT_AUTHENTICATED')
+          throw new Error('ERR_NOT_AUTHENTICATED')
         }
         }
 
 
         const usr = await WIKI.db.users.query().findById(userId)
         const usr = await WIKI.db.users.query().findById(userId)
@@ -224,7 +236,7 @@ export default {
       try {
       try {
         const userId = context.req.user?.id
         const userId = context.req.user?.id
         if (!userId) {
         if (!userId) {
-          throw new Error('ERR_USER_NOT_AUTHENTICATED')
+          throw new Error('ERR_NOT_AUTHENTICATED')
         }
         }
 
 
         const usr = await WIKI.db.users.query().findById(userId)
         const usr = await WIKI.db.users.query().findById(userId)
@@ -283,7 +295,7 @@ export default {
       try {
       try {
         const userId = context.req.user?.id
         const userId = context.req.user?.id
         if (!userId) {
         if (!userId) {
-          throw new Error('ERR_USER_NOT_AUTHENTICATED')
+          throw new Error('ERR_NOT_AUTHENTICATED')
         }
         }
 
 
         const usr = await WIKI.db.users.query().findById(userId)
         const usr = await WIKI.db.users.query().findById(userId)
@@ -346,7 +358,7 @@ export default {
       try {
       try {
         const userId = context.req.user?.id
         const userId = context.req.user?.id
         if (!userId) {
         if (!userId) {
-          throw new Error('ERR_USER_NOT_AUTHENTICATED')
+          throw new Error('ERR_NOT_AUTHENTICATED')
         }
         }
 
 
         const usr = await WIKI.db.users.query().findById(userId)
         const usr = await WIKI.db.users.query().findById(userId)
@@ -584,7 +596,7 @@ export default {
      */
      */
     async revokeApiKey (obj, args, context) {
     async revokeApiKey (obj, args, context) {
       try {
       try {
-        if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:api'])) {
           throw new Error('ERR_FORBIDDEN')
           throw new Error('ERR_FORBIDDEN')
         }
         }
 
 

+ 3 - 0
server/graph/resolvers/block.mjs

@@ -11,6 +11,9 @@ export default {
   Mutation: {
   Mutation: {
     async setBlocksState(obj, args, context) {
     async setBlocksState(obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:blocks'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
         // TODO: update blocks state
         // TODO: update blocks state
         return {
         return {
           operation: generateSuccess('Blocks state updated successfully')
           operation: generateSuccess('Blocks state updated successfully')

+ 25 - 5
server/graph/resolvers/hooks.mjs

@@ -3,10 +3,18 @@ import _ from 'lodash-es'
 
 
 export default {
 export default {
   Query: {
   Query: {
-    async hooks () {
+    async hooks (obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['read:webhooks', 'write:webhooks', 'manage:webhooks'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
       return WIKI.db.hooks.query().orderBy('name')
       return WIKI.db.hooks.query().orderBy('name')
     },
     },
-    async hookById (obj, args) {
+    async hookById (obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['read:webhooks', 'write:webhooks', 'manage:webhooks'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
       return WIKI.db.hooks.query().findById(args.id)
       return WIKI.db.hooks.query().findById(args.id)
     }
     }
   },
   },
@@ -14,8 +22,12 @@ export default {
     /**
     /**
      * CREATE HOOK
      * CREATE HOOK
      */
      */
-    async createHook (obj, args) {
+    async createHook (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['write:webhooks', 'manage:webhooks'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         // -> Validate inputs
         // -> Validate inputs
         if (!args.name || args.name.length < 1) {
         if (!args.name || args.name.length < 1) {
           throw new WIKI.Error.Custom('HookCreateInvalidName', 'Invalid Hook Name')
           throw new WIKI.Error.Custom('HookCreateInvalidName', 'Invalid Hook Name')
@@ -41,8 +53,12 @@ export default {
     /**
     /**
      * UPDATE HOOK
      * UPDATE HOOK
      */
      */
-    async updateHook (obj, args) {
+    async updateHook (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:webhooks'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         // -> Load hook
         // -> Load hook
         const hook = await WIKI.db.hooks.query().findById(args.id)
         const hook = await WIKI.db.hooks.query().findById(args.id)
         if (!hook) {
         if (!hook) {
@@ -72,8 +88,12 @@ export default {
     /**
     /**
      * DELETE HOOK
      * DELETE HOOK
      */
      */
-    async deleteHook (obj, args) {
+    async deleteHook (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:webhooks'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         await WIKI.db.hooks.deleteHook(args.id)
         await WIKI.db.hooks.deleteHook(args.id)
         WIKI.logger.debug(`Hook ${args.id} deleted successfully.`)
         WIKI.logger.debug(`Hook ${args.id} deleted successfully.`)
         return {
         return {

+ 0 - 43
server/graph/resolvers/localization.mjs

@@ -1,6 +1,3 @@
-import { generateError, generateSuccess } from '../../helpers/graph.mjs'
-import _ from 'lodash-es'
-
 export default {
 export default {
   Query: {
   Query: {
     async locales(obj, args, context, info) {
     async locales(obj, args, context, info) {
@@ -9,45 +6,5 @@ export default {
     localeStrings (obj, args, context, info) {
     localeStrings (obj, args, context, info) {
       return WIKI.db.locales.getStrings(args.locale)
       return WIKI.db.locales.getStrings(args.locale)
     }
     }
-  },
-  Mutation: {
-    async downloadLocale(obj, args, context) {
-      try {
-        const job = await WIKI.scheduler.registerJob({
-          name: 'fetch-graph-locale',
-          immediate: true
-        }, args.locale)
-        await job.finished
-        return {
-          responseResult: generateSuccess('Locale downloaded successfully')
-        }
-      } catch (err) {
-        return generateError(err)
-      }
-    },
-    async updateLocale(obj, args, context) {
-      try {
-        WIKI.config.lang.code = args.locale
-        WIKI.config.lang.autoUpdate = args.autoUpdate
-        WIKI.config.lang.namespacing = args.namespacing
-        WIKI.config.lang.namespaces = _.union(args.namespaces, [args.locale])
-
-        const newLocale = await WIKI.db.locales.query().select('isRTL').where('code', args.locale).first()
-        WIKI.config.lang.rtl = newLocale.isRTL
-
-        await WIKI.configSvc.saveToDb(['lang'])
-
-        await WIKI.lang.setCurrentLocale(args.locale)
-        await WIKI.lang.refreshNamespaces()
-
-        await WIKI.cache.del('nav:locales')
-
-        return {
-          responseResult: generateSuccess('Locale config updated')
-        }
-      } catch (err) {
-        return generateError(err)
-      }
-    }
   }
   }
 }
 }

+ 13 - 1
server/graph/resolvers/mail.mjs

@@ -3,7 +3,11 @@ import { generateError, generateSuccess } from '../../helpers/graph.mjs'
 
 
 export default {
 export default {
   Query: {
   Query: {
-    async mailConfig(obj, args, context, info) {
+    async mailConfig(obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
       return {
       return {
         ...WIKI.config.mail,
         ...WIKI.config.mail,
         pass: WIKI.config.mail.pass.length > 0 ? '********' : ''
         pass: WIKI.config.mail.pass.length > 0 ? '********' : ''
@@ -13,6 +17,10 @@ export default {
   Mutation: {
   Mutation: {
     async sendMailTest(obj, args, context) {
     async sendMailTest(obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         if (_.isEmpty(args.recipientEmail) || args.recipientEmail.length < 6) {
         if (_.isEmpty(args.recipientEmail) || args.recipientEmail.length < 6) {
           throw new WIKI.Error.MailInvalidRecipient()
           throw new WIKI.Error.MailInvalidRecipient()
         }
         }
@@ -36,6 +44,10 @@ export default {
     },
     },
     async updateMailConfig(obj, args, context) {
     async updateMailConfig(obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         WIKI.config.mail = {
         WIKI.config.mail = {
           senderName: args.senderName,
           senderName: args.senderName,
           senderEmail: args.senderEmail,
           senderEmail: args.senderEmail,

+ 37 - 20
server/graph/resolvers/page.mjs

@@ -1,6 +1,6 @@
 import _ from 'lodash-es'
 import _ from 'lodash-es'
 import { generateError, generateSuccess } from '../../helpers/graph.mjs'
 import { generateError, generateSuccess } from '../../helpers/graph.mjs'
-import { parsePath }from '../../helpers/page.mjs'
+import { parsePath } from '../../helpers/page.mjs'
 import tsquery from 'pg-tsquery'
 import tsquery from 'pg-tsquery'
 
 
 const tsq = tsquery()
 const tsq = tsquery()
@@ -247,12 +247,19 @@ export default {
         siteId: args.siteId
         siteId: args.siteId
       })
       })
       if (page) {
       if (page) {
-        return {
-          ...page,
-          ...page.config,
-          scriptCss: page.scripts?.css,
-          scriptJsLoad: page.scripts?.jsLoad,
-          scriptJsUnload: page.scripts?.jsUnload
+        if (WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
+          path: page.path,
+          locale: page.locale
+        })) {
+          return {
+            ...page,
+            ...page.config,
+            scriptCss: page.scripts?.css,
+            scriptJsLoad: page.scripts?.jsLoad,
+            scriptJsUnload: page.scripts?.jsUnload
+          }
+        } else {
+          throw new Error('ERR_FORBIDDEN')
         }
         }
       } else {
       } else {
         throw new Error('ERR_PAGE_NOT_FOUND')
         throw new Error('ERR_PAGE_NOT_FOUND')
@@ -265,17 +272,17 @@ export default {
     async pathFromAlias (obj, args, context, info) {
     async pathFromAlias (obj, args, context, info) {
       const alias = args.alias?.trim()
       const alias = args.alias?.trim()
       if (!alias) {
       if (!alias) {
-        throw new Error('ERR_ALIAS_MISSING')
+        throw new Error('ERR_PAGE_ALIAS_MISSING')
       }
       }
       if (!WIKI.sites[args.siteId]) {
       if (!WIKI.sites[args.siteId]) {
-        throw new Error('ERR_INVALID_SITE_ID')
+        throw new Error('ERR_INVALID_SITE')
       }
       }
       const page = await WIKI.db.pages.query().findOne({
       const page = await WIKI.db.pages.query().findOne({
         alias: args.alias,
         alias: args.alias,
         siteId: args.siteId
         siteId: args.siteId
       }).select('id', 'path', 'locale')
       }).select('id', 'path', 'locale')
       if (!page) {
       if (!page) {
-        throw new Error('ERR_ALIAS_NOT_FOUND')
+        throw new Error('ERR_PAGE_ALIAS_NOT_FOUND')
       }
       }
       return {
       return {
         id: page.id,
         id: page.id,
@@ -287,7 +294,7 @@ export default {
      * FETCH TAGS
      * FETCH TAGS
      */
      */
     async tags (obj, args, context, info) {
     async tags (obj, args, context, info) {
-      if (!args.siteId) { throw new Error('Missing Site ID')}
+      if (!args.siteId) { throw new Error('Missing Site ID') }
       const tags = await WIKI.db.knex('tags').where('siteId', args.siteId).orderBy('tag')
       const tags = await WIKI.db.knex('tags').where('siteId', args.siteId).orderBy('tag')
       // TODO: check permissions
       // TODO: check permissions
       return tags
       return tags
@@ -670,19 +677,29 @@ export default {
     }
     }
   },
   },
   Page: {
   Page: {
-    icon (obj) {
-      return obj.icon || 'las la-file-alt'
+    icon (page) {
+      return page.icon || 'las la-file-alt'
+    },
+    password (page) {
+      return page.password ? '********' : ''
     },
     },
-    password (obj) {
-      return obj.password ? '********' : ''
+    content (page, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['read:source', 'write:pages', 'manage:pages'], {
+        path: page.path,
+        locale: page.locale
+      })) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
+      return page.content
     },
     },
-    // async tags (obj) {
-    //   return WIKI.db.pages.relatedQuery('tags').for(obj.id)
+    // async tags (page) {
+    //   return WIKI.db.pages.relatedQuery('tags').for(page.id)
     // },
     // },
-    tocDepth (obj) {
+    tocDepth (page) {
       return {
       return {
-        min: obj.extra?.tocDepth?.min ?? 1,
-        max: obj.extra?.tocDepth?.max ?? 2
+        min: page.extra?.tocDepth?.min ?? 1,
+        max: page.extra?.tocDepth?.max ?? 2
       }
       }
     }
     }
     // comments(pg) {
     // comments(pg) {

+ 27 - 3
server/graph/resolvers/site.mjs

@@ -49,8 +49,12 @@ export default {
     /**
     /**
      * CREATE SITE
      * CREATE SITE
      */
      */
-    async createSite (obj, args) {
+    async createSite (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['write:sites', 'manage:sites'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         // -> Validate inputs
         // -> Validate inputs
         if (!args.hostname || args.hostname.length < 1 || !/^(\\*)|([a-z0-9\-.:]+)$/.test(args.hostname)) {
         if (!args.hostname || args.hostname.length < 1 || !/^(\\*)|([a-z0-9\-.:]+)$/.test(args.hostname)) {
           throw new WIKI.Error.Custom('SiteCreateInvalidHostname', 'Invalid Site Hostname')
           throw new WIKI.Error.Custom('SiteCreateInvalidHostname', 'Invalid Site Hostname')
@@ -83,8 +87,12 @@ export default {
     /**
     /**
      * UPDATE SITE
      * UPDATE SITE
      */
      */
-    async updateSite (obj, args) {
+    async updateSite (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         // -> Load site
         // -> Load site
         const site = await WIKI.db.sites.query().findById(args.id)
         const site = await WIKI.db.sites.query().findById(args.id)
         if (!site) {
         if (!site) {
@@ -127,8 +135,12 @@ export default {
     /**
     /**
      * DELETE SITE
      * DELETE SITE
      */
      */
-    async deleteSite (obj, args) {
+    async deleteSite (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         // -> Ensure site isn't last one
         // -> Ensure site isn't last one
         const sitesCount = await WIKI.db.sites.query().count('id').first()
         const sitesCount = await WIKI.db.sites.query().count('id').first()
         if (sitesCount?.count && _.toNumber(sitesCount?.count) <= 1) {
         if (sitesCount?.count && _.toNumber(sitesCount?.count) <= 1) {
@@ -149,6 +161,10 @@ export default {
      */
      */
     async uploadSiteLogo (obj, args, context) {
     async uploadSiteLogo (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         const { filename, mimetype, createReadStream } = await args.image
         const { filename, mimetype, createReadStream } = await args.image
         WIKI.logger.info(`Processing site logo ${filename} of type ${mimetype}...`)
         WIKI.logger.info(`Processing site logo ${filename} of type ${mimetype}...`)
         if (!WIKI.extensions.ext.sharp.isInstalled) {
         if (!WIKI.extensions.ext.sharp.isInstalled) {
@@ -208,6 +224,10 @@ export default {
      */
      */
     async uploadSiteFavicon (obj, args, context) {
     async uploadSiteFavicon (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         const { filename, mimetype, createReadStream } = await args.image
         const { filename, mimetype, createReadStream } = await args.image
         WIKI.logger.info(`Processing site favicon ${filename} of type ${mimetype}...`)
         WIKI.logger.info(`Processing site favicon ${filename} of type ${mimetype}...`)
         if (!WIKI.extensions.ext.sharp.isInstalled) {
         if (!WIKI.extensions.ext.sharp.isInstalled) {
@@ -268,6 +288,10 @@ export default {
      */
      */
     async uploadSiteLoginBg (obj, args, context) {
     async uploadSiteLoginBg (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         const { filename, mimetype, createReadStream } = await args.image
         const { filename, mimetype, createReadStream } = await args.image
         WIKI.logger.info(`Processing site login bg ${filename} of type ${mimetype}...`)
         WIKI.logger.info(`Processing site login bg ${filename} of type ${mimetype}...`)
         if (!WIKI.extensions.ext.sharp.isInstalled) {
         if (!WIKI.extensions.ext.sharp.isInstalled) {

+ 165 - 40
server/graph/resolvers/system.mjs

@@ -12,18 +12,44 @@ const getos = util.promisify(getosSync)
 
 
 export default {
 export default {
   Query: {
   Query: {
+    /**
+     * System Flags
+     */
     systemFlags () {
     systemFlags () {
       return WIKI.config.flags
       return WIKI.config.flags
     },
     },
-    async systemInfo () { return {} },
-    async systemExtensions () {
+    /**
+     * System Info
+     */
+    async systemInfo (obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['read:dashboard', 'manage:sites'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
+      return {}
+    },
+    /**
+     * System Extensions
+     */
+    async systemExtensions (obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
       const exts = Object.values(WIKI.extensions.ext).map(ext => _.pick(ext, ['key', 'title', 'description', 'isInstalled', 'isInstallable']))
       const exts = Object.values(WIKI.extensions.ext).map(ext => _.pick(ext, ['key', 'title', 'description', 'isInstalled', 'isInstallable']))
       for (const ext of exts) {
       for (const ext of exts) {
         ext.isCompatible = await WIKI.extensions.ext[ext.key].isCompatible()
         ext.isCompatible = await WIKI.extensions.ext[ext.key].isCompatible()
       }
       }
       return exts
       return exts
     },
     },
-    async systemInstances () {
+    /**
+     * List System Instances
+     */
+    async systemInstances (obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
       const instRaw = await WIKI.db.knex('pg_stat_activity')
       const instRaw = await WIKI.db.knex('pg_stat_activity')
         .select([
         .select([
           'usename',
           'usename',
@@ -56,10 +82,24 @@ export default {
       }
       }
       return _.values(insts)
       return _.values(insts)
     },
     },
-    systemSecurity () {
+    /**
+     * System Security Settings
+     */
+    systemSecurity (obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
       return WIKI.config.security
       return WIKI.config.security
     },
     },
-    async systemJobs (obj, args) {
+    /**
+     * List System Jobs
+     */
+    async systemJobs (obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
       const results = args.states?.length > 0 ?
       const results = args.states?.length > 0 ?
         await WIKI.db.knex('jobHistory').whereIn('state', args.states.map(s => s.toLowerCase())).orderBy('startedAt', 'desc') :
         await WIKI.db.knex('jobHistory').whereIn('state', args.states.map(s => s.toLowerCase())).orderBy('startedAt', 'desc') :
         await WIKI.db.knex('jobHistory').orderBy('startedAt', 'desc')
         await WIKI.db.knex('jobHistory').orderBy('startedAt', 'desc')
@@ -68,16 +108,37 @@ export default {
         state: r.state.toUpperCase()
         state: r.state.toUpperCase()
       }))
       }))
     },
     },
-    async systemJobsScheduled (obj, args) {
+    /**
+     * List Scheduled Jobs
+     */
+    async systemJobsScheduled (obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
       return WIKI.db.knex('jobSchedule').orderBy('task')
       return WIKI.db.knex('jobSchedule').orderBy('task')
     },
     },
-    async systemJobsUpcoming (obj, args) {
+    /**
+     * List Upcoming Jobs
+     */
+    async systemJobsUpcoming (obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
       return WIKI.db.knex('jobs').orderBy([
       return WIKI.db.knex('jobs').orderBy([
         { column: 'waitUntil', order: 'asc', nulls: 'first' },
         { column: 'waitUntil', order: 'asc', nulls: 'first' },
         { column: 'createdAt', order: 'asc' }
         { column: 'createdAt', order: 'asc' }
       ])
       ])
     },
     },
-    systemSearch () {
+    /**
+     * Search Settings
+     */
+    systemSearch (obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
       return {
       return {
         ...WIKI.config.search,
         ...WIKI.config.search,
         dictOverrides: JSON.stringify(WIKI.config.search.dictOverrides, null, 2)
         dictOverrides: JSON.stringify(WIKI.config.search.dictOverrides, null, 2)
@@ -86,8 +147,13 @@ export default {
   },
   },
   Mutation: {
   Mutation: {
     async cancelJob (obj, args, context) {
     async cancelJob (obj, args, context) {
-      WIKI.logger.info(`Admin requested cancelling job ${args.id}...`)
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
+        WIKI.logger.info(`Admin requested cancelling job ${args.id}...`)
+
         const result = await WIKI.db.knex('jobs')
         const result = await WIKI.db.knex('jobs')
           .where('id', args.id)
           .where('id', args.id)
           .del()
           .del()
@@ -100,12 +166,18 @@ export default {
           operation: generateSuccess('Cancelled job successfully.')
           operation: generateSuccess('Cancelled job successfully.')
         }
         }
       } catch (err) {
       } catch (err) {
-        WIKI.logger.warn(err)
+        if (err.message !== 'ERR_FORBIDDEN') {
+          WIKI.logger.warn(err)
+        }
         return generateError(err)
         return generateError(err)
       }
       }
     },
     },
     async checkForUpdates (obj, args, context) {
     async checkForUpdates (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['read:dashboard', 'manage:system'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         const renderJob = await WIKI.scheduler.addJob({
         const renderJob = await WIKI.scheduler.addJob({
           task: 'checkVersion',
           task: 'checkVersion',
           maxRetries: 0,
           maxRetries: 0,
@@ -119,15 +191,28 @@ export default {
           latestDate: WIKI.config.update.versionDate
           latestDate: WIKI.config.update.versionDate
         }
         }
       } catch (err) {
       } catch (err) {
-        WIKI.logger.warn(err)
+        if (err.message !== 'ERR_FORBIDDEN') {
+          WIKI.logger.warn(err)
+        }
         return generateError(err)
         return generateError(err)
       }
       }
     },
     },
     async disconnectWS (obj, args, context) {
     async disconnectWS (obj, args, context) {
-      WIKI.servers.ws.disconnectSockets(true)
-      WIKI.logger.info('All active websocket connections have been terminated.')
-      return {
-        operation: generateSuccess('All websocket connections closed successfully.')
+      try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
+        WIKI.servers.ws.disconnectSockets(true)
+        WIKI.logger.info('All active websocket connections have been terminated.')
+        return {
+          operation: generateSuccess('All websocket connections closed successfully.')
+        }
+      } catch (err) {
+        if (err.message !== 'ERR_FORBIDDEN') {
+          WIKI.logger.warn(err)
+        }
+        return generateError(err)
       }
       }
     },
     },
     async installExtension (obj, args, context) {
     async installExtension (obj, args, context) {
@@ -143,6 +228,10 @@ export default {
     },
     },
     async rebuildSearchIndex (obj, args, context) {
     async rebuildSearchIndex (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         await WIKI.scheduler.addJob({
         await WIKI.scheduler.addJob({
           task: 'rebuildSearchIndex',
           task: 'rebuildSearchIndex',
           maxRetries: 0
           maxRetries: 0
@@ -155,8 +244,13 @@ export default {
       }
       }
     },
     },
     async retryJob (obj, args, context) {
     async retryJob (obj, args, context) {
-      WIKI.logger.info(`Admin requested rescheduling of job ${args.id}...`)
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
+        WIKI.logger.info(`Admin requested rescheduling of job ${args.id}...`)
+
         const job = await WIKI.db.knex('jobHistory')
         const job = await WIKI.db.knex('jobHistory')
           .where('id', args.id)
           .where('id', args.id)
           .first()
           .first()
@@ -188,34 +282,61 @@ export default {
       }
       }
     },
     },
     async updateSystemFlags (obj, args, context) {
     async updateSystemFlags (obj, args, context) {
-      WIKI.config.flags = {
-        ...WIKI.config.flags,
-        ...args.flags
-      }
-      await WIKI.configSvc.applyFlags()
-      await WIKI.configSvc.saveToDb(['flags'])
-      return {
-        operation: generateSuccess('System Flags applied successfully')
+      try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
+        WIKI.config.flags = {
+          ...WIKI.config.flags,
+          ...args.flags
+        }
+        await WIKI.configSvc.applyFlags()
+        await WIKI.configSvc.saveToDb(['flags'])
+        return {
+          operation: generateSuccess('System Flags applied successfully')
+        }
+      } catch (err) {
+        WIKI.logger.warn(err)
+        return generateError(err)
       }
       }
     },
     },
     async updateSystemSearch (obj, args, context) {
     async updateSystemSearch (obj, args, context) {
-      WIKI.config.search = {
-        ...WIKI.config.search,
-        termHighlighting: args.termHighlighting ?? WIKI.config.search.termHighlighting,
-        dictOverrides: args.dictOverrides ? JSON.parse(args.dictOverrides) : WIKI.config.search.dictOverrides
-      }
-      // TODO: broadcast config update
-      await WIKI.configSvc.saveToDb(['search'])
-      return {
-        operation: generateSuccess('System Search configuration applied successfully')
+      try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
+        WIKI.config.search = {
+          ...WIKI.config.search,
+          termHighlighting: args.termHighlighting ?? WIKI.config.search.termHighlighting,
+          dictOverrides: args.dictOverrides ? JSON.parse(args.dictOverrides) : WIKI.config.search.dictOverrides
+        }
+        // TODO: broadcast config update
+        await WIKI.configSvc.saveToDb(['search'])
+        return {
+          operation: generateSuccess('System Search configuration applied successfully')
+        }
+      } catch (err) {
+        WIKI.logger.warn(err)
+        return generateError(err)
       }
       }
     },
     },
     async updateSystemSecurity (obj, args, context) {
     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 {
-        operation: generateSuccess('System Security configuration applied successfully')
+      try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
+        WIKI.config.security = _.defaultsDeep(_.omit(args, ['__typename']), WIKI.config.security)
+        // TODO: broadcast config update
+        await WIKI.configSvc.saveToDb(['security'])
+        return {
+          operation: generateSuccess('System Security configuration applied successfully')
+        }
+      } catch (err) {
+        WIKI.logger.warn(err)
+        return generateError(err)
       }
       }
     }
     }
   },
   },
@@ -310,12 +431,16 @@ export default {
       const total = await WIKI.db.pages.query().count('* as total').first()
       const total = await WIKI.db.pages.query().count('* as total').first()
       return _.toSafeInteger(total.total)
       return _.toSafeInteger(total.total)
     },
     },
+    async tagsTotal () {
+      const total = await WIKI.db.tags.query().count('* as total').first()
+      return _.toSafeInteger(total.total)
+    },
     async usersTotal () {
     async usersTotal () {
       const total = await WIKI.db.users.query().count('* as total').first()
       const total = await WIKI.db.users.query().count('* as total').first()
       return _.toSafeInteger(total.total)
       return _.toSafeInteger(total.total)
     },
     },
-    async tagsTotal () {
-      const total = await WIKI.db.tags.query().count('* as total').first()
+    async loginsPastDay () {
+      const total = await WIKI.db.users.query().count('* as total').whereRaw('"lastLoginAt" >= NOW() - INTERVAL \'1 DAY\'').first()
       return _.toSafeInteger(total.total)
       return _.toSafeInteger(total.total)
     }
     }
   }
   }

+ 2 - 2
server/graph/resolvers/tree.mjs

@@ -133,7 +133,7 @@ export default {
         .first()
         .first()
 
 
       if (!folder) {
       if (!folder) {
-        throw new Error('ERR_FOLDER_NOT_EXIST')
+        throw new Error('ERR_INVALID_FOLDER')
       }
       }
 
 
       return {
       return {
@@ -158,7 +158,7 @@ export default {
         .first()
         .first()
 
 
       if (!folder) {
       if (!folder) {
-        throw new Error('ERR_FOLDER_NOT_EXIST')
+        throw new Error('ERR_INVALID_FOLDER')
       }
       }
 
 
       return {
       return {

+ 106 - 92
server/graph/resolvers/user.mjs

@@ -3,6 +3,7 @@ import _, { isNil } from 'lodash-es'
 import path from 'node:path'
 import path from 'node:path'
 import fs from 'fs-extra'
 import fs from 'fs-extra'
 import { DateTime } from 'luxon'
 import { DateTime } from 'luxon'
+import bcrypt from 'bcryptjs'
 
 
 export default {
 export default {
   Query: {
   Query: {
@@ -10,6 +11,10 @@ export default {
      * FETCH ALL USERS
      * FETCH ALL USERS
      */
      */
     async users (obj, args, context, info) {
     async users (obj, args, context, info) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['read:users', 'write:users', 'manage:users'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
       // -> Sanitize limit
       // -> Sanitize limit
       let limit = args.pageSize ?? 20
       let limit = args.pageSize ?? 20
       if (limit < 1 || limit > 1000) {
       if (limit < 1 || limit > 1000) {
@@ -39,6 +44,12 @@ export default {
      * FETCH A SINGLE USER
      * FETCH A SINGLE USER
      */
      */
     async userById (obj, args, context, info) {
     async userById (obj, args, context, info) {
+      if (!context.req.isAuthenticated || context.req.user.id !== args.id) {
+        if (!WIKI.auth.checkAccess(context.req.user, ['read:users', 'write:users', 'manage:users'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+      }
+
       const usr = await WIKI.db.users.query().findById(args.id)
       const usr = await WIKI.db.users.query().findById(args.id)
 
 
       if (!usr) {
       if (!usr) {
@@ -69,31 +80,20 @@ export default {
 
 
       return usr
       return usr
     },
     },
-    // async profile (obj, args, context, info) {
-    //   if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) {
-    //     throw new WIKI.Error.AuthRequired()
-    //   }
-    //   const usr = await WIKI.db.users.query().findById(context.req.user.id)
-    //   if (!usr.isActive) {
-    //     throw new WIKI.Error.AuthAccountBanned()
-    //   }
-
-    //   const providerInfo = _.get(WIKI.auth.strategies, usr.providerKey, {})
-
-    //   usr.providerName = providerInfo.displayName || 'Unknown'
-    //   usr.lastLoginAt = usr.lastLoginAt || usr.updatedAt
-    //   usr.password = ''
-    //   usr.providerId = ''
-    //   usr.tfaSecret = ''
-
-    //   return usr
-    // },
 
 
     async userDefaults (obj, args, context) {
     async userDefaults (obj, args, context) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['read:users', 'write:users', 'manage:users'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
       return WIKI.config.userDefaults
       return WIKI.config.userDefaults
     },
     },
 
 
     async lastLogins (obj, args, context, info) {
     async lastLogins (obj, args, context, info) {
+      if (!WIKI.auth.checkAccess(context.req.user, ['read:dashboard', 'read:users', 'write:users', 'manage:users'])) {
+        throw new Error('ERR_FORBIDDEN')
+      }
+
       return WIKI.db.users.query()
       return WIKI.db.users.query()
         .select('id', 'name', 'lastLoginAt')
         .select('id', 'name', 'lastLoginAt')
         .whereNotNull('lastLoginAt')
         .whereNotNull('lastLoginAt')
@@ -114,8 +114,12 @@ export default {
     }
     }
   },
   },
   Mutation: {
   Mutation: {
-    async createUser (obj, args) {
+    async createUser (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['write:users', 'manage:users'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         await WIKI.db.users.createNewUser({ ...args, isVerified: true })
         await WIKI.db.users.createNewUser({ ...args, isVerified: true })
 
 
         return {
         return {
@@ -125,8 +129,12 @@ export default {
         return generateError(err)
         return generateError(err)
       }
       }
     },
     },
-    async deleteUser (obj, args) {
+    async deleteUser (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:users'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         if (args.id <= 2) {
         if (args.id <= 2) {
           throw new WIKI.Error.UserDeleteProtected()
           throw new WIKI.Error.UserDeleteProtected()
         }
         }
@@ -146,8 +154,12 @@ export default {
         }
         }
       }
       }
     },
     },
-    async updateUser (obj, args) {
+    async updateUser (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:users'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         await WIKI.db.users.updateUser(args.id, args.patch)
         await WIKI.db.users.updateUser(args.id, args.patch)
 
 
         return {
         return {
@@ -157,8 +169,12 @@ export default {
         return generateError(err)
         return generateError(err)
       }
       }
     },
     },
-    async verifyUser (obj, args) {
+    async verifyUser (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:users'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         await WIKI.db.users.query().patch({ isVerified: true }).findById(args.id)
         await WIKI.db.users.query().patch({ isVerified: true }).findById(args.id)
 
 
         return {
         return {
@@ -168,8 +184,12 @@ export default {
         return generateError(err)
         return generateError(err)
       }
       }
     },
     },
-    async activateUser (obj, args) {
+    async activateUser (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:users'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         await WIKI.db.users.query().patch({ isActive: true }).findById(args.id)
         await WIKI.db.users.query().patch({ isActive: true }).findById(args.id)
 
 
         return {
         return {
@@ -179,8 +199,12 @@ export default {
         return generateError(err)
         return generateError(err)
       }
       }
     },
     },
-    async deactivateUser (obj, args) {
+    async deactivateUser (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:users'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         if (args.id <= 2) {
         if (args.id <= 2) {
           throw new Error('Cannot deactivate system accounts.')
           throw new Error('Cannot deactivate system accounts.')
         }
         }
@@ -196,8 +220,12 @@ export default {
         return generateError(err)
         return generateError(err)
       }
       }
     },
     },
-    async enableUserTFA (obj, args) {
+    async enableUserTFA (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:users'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         await WIKI.db.users.query().patch({ tfaIsActive: true, tfaSecret: null }).findById(args.id)
         await WIKI.db.users.query().patch({ tfaIsActive: true, tfaSecret: null }).findById(args.id)
 
 
         return {
         return {
@@ -207,8 +235,12 @@ export default {
         return generateError(err)
         return generateError(err)
       }
       }
     },
     },
-    async disableUserTFA (obj, args) {
+    async disableUserTFA (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:users'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         await WIKI.db.users.query().patch({ tfaIsActive: false, tfaSecret: null }).findById(args.id)
         await WIKI.db.users.query().patch({ tfaIsActive: false, tfaSecret: null }).findById(args.id)
 
 
         return {
         return {
@@ -220,13 +252,17 @@ export default {
     },
     },
     async changeUserPassword (obj, args, context) {
     async changeUserPassword (obj, args, context) {
       try {
       try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:users'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
         if (args.newPassword?.length < 8) {
         if (args.newPassword?.length < 8) {
           throw new Error('ERR_PASSWORD_TOO_SHORT')
           throw new Error('ERR_PASSWORD_TOO_SHORT')
         }
         }
 
 
         const usr = await WIKI.db.users.query().findById(args.id)
         const usr = await WIKI.db.users.query().findById(args.id)
         if (!usr) {
         if (!usr) {
-          throw new Error('ERR_USER_NOT_FOUND')
+          throw new Error('ERR_INVALID_USER')
         }
         }
         const localAuth = await WIKI.db.authentication.getStrategy('local')
         const localAuth = await WIKI.db.authentication.getStrategy('local')
 
 
@@ -249,22 +285,22 @@ export default {
     async updateProfile (obj, args, context) {
     async updateProfile (obj, args, context) {
       try {
       try {
         if (!context.req.user || context.req.user.id === WIKI.auth.guest.id) {
         if (!context.req.user || context.req.user.id === WIKI.auth.guest.id) {
-          throw new WIKI.Error.AuthRequired()
+          throw new Error('ERR_NOT_AUTHENTICATED')
         }
         }
         const usr = await WIKI.db.users.query().findById(context.req.user.id)
         const usr = await WIKI.db.users.query().findById(context.req.user.id)
         if (!usr.isActive) {
         if (!usr.isActive) {
-          throw new WIKI.Error.AuthAccountBanned()
+          throw new Error('ERR_INACTIVE_USER')
         }
         }
         if (!usr.isVerified) {
         if (!usr.isVerified) {
-          throw new WIKI.Error.AuthAccountNotVerified()
+          throw new Error('ERR_USER_NOT_VERIFIED')
         }
         }
 
 
         if (args.dateFormat && !['', 'DD/MM/YYYY', 'DD.MM.YYYY', 'MM/DD/YYYY', 'YYYY-MM-DD', 'YYYY/MM/DD'].includes(args.dateFormat)) {
         if (args.dateFormat && !['', 'DD/MM/YYYY', 'DD.MM.YYYY', 'MM/DD/YYYY', 'YYYY-MM-DD', 'YYYY/MM/DD'].includes(args.dateFormat)) {
-          throw new WIKI.Error.InputInvalid()
+          throw new Error('ERR_INVALID_INPUT')
         }
         }
 
 
         if (args.appearance && !['site', 'light', 'dark'].includes(args.appearance)) {
         if (args.appearance && !['site', 'light', 'dark'].includes(args.appearance)) {
-          throw new WIKI.Error.InputInvalid()
+          throw new Error('ERR_INVALID_INPUT')
         }
         }
 
 
         await WIKI.db.users.query().findById(usr.id).patch({
         await WIKI.db.users.query().findById(usr.id).patch({
@@ -292,47 +328,19 @@ export default {
         return generateError(err)
         return generateError(err)
       }
       }
     },
     },
-    // async changePassword (obj, args, context) {
-    //   try {
-    //     if (!context.req.user || context.req.user.id < 1 || context.req.user.id === 2) {
-    //       throw new WIKI.Error.AuthRequired()
-    //     }
-    //     const usr = await WIKI.db.users.query().findById(context.req.user.id)
-    //     if (!usr.isActive) {
-    //       throw new WIKI.Error.AuthAccountBanned()
-    //     }
-    //     if (!usr.isVerified) {
-    //       throw new WIKI.Error.AuthAccountNotVerified()
-    //     }
-    //     if (usr.providerKey !== 'local') {
-    //       throw new WIKI.Error.AuthProviderInvalid()
-    //     }
-    //     try {
-    //       await usr.verifyPassword(args.current)
-    //     } catch (err) {
-    //       throw new WIKI.Error.AuthPasswordInvalid()
-    //     }
-
-    //     await WIKI.db.users.updateUser({
-    //       id: usr.id,
-    //       newPassword: args.new
-    //     })
-
-    //     const newToken = await WIKI.db.users.refreshToken(usr)
-
-    //     return {
-    //       responseResult: generateSuccess('Password changed successfully'),
-    //       jwt: newToken.token
-    //     }
-    //   } catch (err) {
-    //     return generateError(err)
-    //   }
-    // },
     /**
     /**
      * UPLOAD USER AVATAR
      * UPLOAD USER AVATAR
      */
      */
-    async uploadUserAvatar (obj, args) {
+    async uploadUserAvatar (obj, args, context) {
       try {
       try {
+        if (!context.req.user || context.req.user.id === WIKI.auth.guest.id) {
+          throw new Error('ERR_NOT_AUTHENTICATED')
+        }
+        const usr = await WIKI.db.users.query().findById(context.req.user.id)
+        if (!usr) {
+          throw new Error('ERR_INVALID_USER')
+        }
+
         const { filename, mimetype, createReadStream } = await args.image
         const { filename, mimetype, createReadStream } = await args.image
         const lowercaseFilename = filename.toLowerCase()
         const lowercaseFilename = filename.toLowerCase()
         WIKI.logger.debug(`Processing user ${args.id} avatar ${lowercaseFilename} of type ${mimetype}...`)
         WIKI.logger.debug(`Processing user ${args.id} avatar ${lowercaseFilename} of type ${mimetype}...`)
@@ -358,7 +366,7 @@ export default {
           height: 180
           height: 180
         })
         })
         // -> Set avatar flag for this user in the DB
         // -> Set avatar flag for this user in the DB
-        await WIKI.db.users.query().findById(args.id).patch({ hasAvatar: true })
+        usr.$query().patch({ hasAvatar: true })
         // -> Save image data to DB
         // -> Save image data to DB
         const imgBuffer = await fs.readFile(destPath)
         const imgBuffer = await fs.readFile(destPath)
         await WIKI.db.knex('userAvatars').insert({
         await WIKI.db.knex('userAvatars').insert({
@@ -377,10 +385,18 @@ export default {
     /**
     /**
      * CLEAR USER AVATAR
      * CLEAR USER AVATAR
      */
      */
-    async clearUserAvatar (obj, args) {
+    async clearUserAvatar (obj, args, context) {
       try {
       try {
+        if (!context.req.user || context.req.user.id === WIKI.auth.guest.id) {
+          throw new Error('ERR_NOT_AUTHENTICATED')
+        }
+        const usr = await WIKI.db.users.query().findById(context.req.user.id)
+        if (!usr) {
+          throw new Error('ERR_INVALID_USER')
+        }
+
         WIKI.logger.debug(`Clearing user ${args.id} avatar...`)
         WIKI.logger.debug(`Clearing user ${args.id} avatar...`)
-        await WIKI.db.users.query().findById(args.id).patch({ hasAvatar: false })
+        usr.$query.patch({ hasAvatar: false })
         await WIKI.db.knex('userAvatars').where({ id: args.id }).del()
         await WIKI.db.knex('userAvatars').where({ id: args.id }).del()
         WIKI.logger.debug(`Cleared user ${args.id} avatar successfully.`)
         WIKI.logger.debug(`Cleared user ${args.id} avatar successfully.`)
         return {
         return {
@@ -395,19 +411,27 @@ export default {
      * UPDATE USER DEFAULTS
      * UPDATE USER DEFAULTS
      */
      */
     async updateUserDefaults (obj, args, context) {
     async updateUserDefaults (obj, args, context) {
-      WIKI.config.userDefaults = {
-        timezone: args.timezone,
-        dateFormat: args.dateFormat,
-        timeFormat: args.timeFormat
-      }
-      await WIKI.configSvc.saveToDb(['userDefaults'])
-      return {
-        operation: generateSuccess('User defaults saved successfully')
+      try {
+        if (!WIKI.auth.checkAccess(context.req.user, ['manage:users'])) {
+          throw new Error('ERR_FORBIDDEN')
+        }
+
+        WIKI.config.userDefaults = {
+          timezone: args.timezone,
+          dateFormat: args.dateFormat,
+          timeFormat: args.timeFormat
+        }
+        await WIKI.configSvc.saveToDb(['userDefaults'])
+        return {
+          operation: generateSuccess('User defaults saved successfully')
+        }
+      } catch (err) {
+        return generateError(err)
       }
       }
     }
     }
   },
   },
   User: {
   User: {
-    async auth (usr) {
+    async auth (usr, args, context) {
       const authStrategies = await WIKI.db.authentication.getStrategies({ enabledOnly: true })
       const authStrategies = await WIKI.db.authentication.getStrategies({ enabledOnly: true })
       return _.transform(usr.auth, (result, value, key) => {
       return _.transform(usr.auth, (result, value, key) => {
         const authStrategy = _.find(authStrategies, ['id', key])
         const authStrategy = _.find(authStrategies, ['id', key])
@@ -432,14 +456,4 @@ export default {
       return usr.$relatedQuery('groups')
       return usr.$relatedQuery('groups')
     }
     }
   }
   }
-  // UserProfile: {
-  //   async groups (usr) {
-  //     const usrGroups = await usr.$relatedQuery('groups')
-  //     return usrGroups.map(g => g.name)
-  //   },
-  //   async pagesTotal (usr) {
-  //     const result = await WIKI.db.pages.query().count('* as total').where('creatorId', usr.id).first()
-  //     return _.toSafeInteger(result.total)
-  //   }
-  // }
 }
 }

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

@@ -7,19 +7,6 @@ extend type Query {
   localeStrings(locale: String!): JSON
   localeStrings(locale: String!): JSON
 }
 }
 
 
-extend type Mutation {
-  downloadLocale(
-    locale: String!
-  ): DefaultResponse
-
-  updateLocale(
-    locale: String!
-    autoUpdate: Boolean!
-    namespacing: Boolean!
-    namespaces: [String]!
-  ): DefaultResponse
-}
-
 # -----------------------------------------------
 # -----------------------------------------------
 # TYPES
 # TYPES
 # -----------------------------------------------
 # -----------------------------------------------

+ 1 - 0
server/graph/schemas/system.graphql

@@ -86,6 +86,7 @@ type SystemInfo {
   isSchedulerHealthy: Boolean
   isSchedulerHealthy: Boolean
   latestVersion: String
   latestVersion: String
   latestVersionReleaseDate: Date
   latestVersionReleaseDate: Date
+  loginsPastDay: Int
   nodeVersion: String
   nodeVersion: String
   operatingSystem: String
   operatingSystem: String
   pagesTotal: Int
   pagesTotal: Int

+ 1 - 1
server/models/pages.mjs

@@ -227,7 +227,7 @@ export class Page extends Model {
   static async createPage(opts) {
   static async createPage(opts) {
     // -> Validate site
     // -> Validate site
     if (!WIKI.sites[opts.siteId]) {
     if (!WIKI.sites[opts.siteId]) {
-      throw new Error('ERR_INVALID_SITE_ID')
+      throw new Error('ERR_INVALID_SITE')
     }
     }
 
 
     // -> Remove trailing slash
     // -> Remove trailing slash

+ 12 - 12
server/models/tree.mjs

@@ -77,7 +77,7 @@ export class Tree extends Model {
     if (id) {
     if (id) {
       const parent = await WIKI.db.knex('tree').where('id', id).first()
       const parent = await WIKI.db.knex('tree').where('id', id).first()
       if (!parent) {
       if (!parent) {
-        throw new Error('ERR_NONEXISTING_FOLDER_ID')
+        throw new Error('ERR_INVALID_FOLDER')
       }
       }
       return parent
       return parent
     } else {
     } else {
@@ -105,7 +105,7 @@ export class Tree extends Model {
           siteId
           siteId
         })
         })
       } else {
       } else {
-        throw new Error('ERR_NONEXISTING_FOLDER_PATH')
+        throw new Error('ERR_INVALID_FOLDER')
       }
       }
     }
     }
   }
   }
@@ -150,7 +150,7 @@ export class Tree extends Model {
       siteId,
       siteId,
       tags,
       tags,
       meta,
       meta,
-      navigationId: siteId,
+      navigationId: siteId
     }).returning('*')
     }).returning('*')
 
 
     return pageEntry[0]
     return pageEntry[0]
@@ -215,12 +215,12 @@ export class Tree extends Model {
   static async createFolder ({ parentId, parentPath, pathName, title, locale, siteId }) {
   static async createFolder ({ parentId, parentPath, pathName, title, locale, siteId }) {
     // Validate path name
     // Validate path name
     if (!rePathName.test(pathName)) {
     if (!rePathName.test(pathName)) {
-      throw new Error('ERR_INVALID_PATH_NAME')
+      throw new Error('ERR_INVALID_PATH')
     }
     }
 
 
     // Validate title
     // Validate title
     if (!reTitle.test(title)) {
     if (!reTitle.test(title)) {
-      throw new Error('ERR_INVALID_TITLE')
+      throw new Error('ERR_FOLDER_TITLE_INVALID')
     }
     }
 
 
     parentPath = encodeTreePath(parentPath)
     parentPath = encodeTreePath(parentPath)
@@ -236,7 +236,7 @@ export class Tree extends Model {
     if (parentId) {
     if (parentId) {
       parent = await WIKI.db.knex('tree').where('id', parentId).first()
       parent = await WIKI.db.knex('tree').where('id', parentId).first()
       if (!parent) {
       if (!parent) {
-        throw new Error('ERR_NONEXISTING_PARENT_ID')
+        throw new Error('ERR_FOLDER_PARENT_INVALID')
       }
       }
       parentPath = parent.folderPath ? `${decodeFolderPath(parent.folderPath)}.${parent.fileName}` : parent.fileName
       parentPath = parent.folderPath ? `${decodeFolderPath(parent.folderPath)}.${parent.fileName}` : parent.fileName
     } else if (parentPath) {
     } else if (parentPath) {
@@ -254,7 +254,7 @@ export class Tree extends Model {
       type: 'folder'
       type: 'folder'
     }).first()
     }).first()
     if (existingFolder) {
     if (existingFolder) {
-      throw new Error('ERR_FOLDER_ALREADY_EXISTS')
+      throw new Error('ERR_FOLDER_DUPLICATE')
     }
     }
 
 
     // Ensure all ancestors exist
     // Ensure all ancestors exist
@@ -338,17 +338,17 @@ export class Tree extends Model {
     // Get folder
     // Get folder
     const folder = await WIKI.db.knex('tree').where('id', folderId).first()
     const folder = await WIKI.db.knex('tree').where('id', folderId).first()
     if (!folder) {
     if (!folder) {
-      throw new Error('ERR_NONEXISTING_FOLDER_ID')
+      throw new Error('ERR_INVALID_FOLDER')
     }
     }
 
 
     // Validate path name
     // Validate path name
     if (!rePathName.test(pathName)) {
     if (!rePathName.test(pathName)) {
-      throw new Error('ERR_INVALID_PATH_NAME')
+      throw new Error('ERR_INVALID_PATH')
     }
     }
 
 
     // Validate title
     // Validate title
     if (!reTitle.test(title)) {
     if (!reTitle.test(title)) {
-      throw new Error('ERR_INVALID_TITLE')
+      throw new Error('ERR_FOLDER_TITLE_INVALID')
     }
     }
 
 
     WIKI.logger.debug(`Renaming folder ${folder.id} path to ${pathName}...`)
     WIKI.logger.debug(`Renaming folder ${folder.id} path to ${pathName}...`)
@@ -364,7 +364,7 @@ export class Tree extends Model {
           type: 'folder'
           type: 'folder'
         }).first()
         }).first()
       if (existingFolder) {
       if (existingFolder) {
-        throw new Error('ERR_FOLDER_ALREADY_EXISTS')
+        throw new Error('ERR_FOLDER_DUPLICATE')
       }
       }
 
 
       // Build new paths
       // Build new paths
@@ -406,7 +406,7 @@ export class Tree extends Model {
     // Get folder
     // Get folder
     const folder = await WIKI.db.knex('tree').where('id', folderId).first()
     const folder = await WIKI.db.knex('tree').where('id', folderId).first()
     if (!folder) {
     if (!folder) {
-      throw new Error('ERR_NONEXISTING_FOLDER_ID')
+      throw new Error('ERR_INVALID_FOLDER')
     }
     }
     const folderPath = folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName
     const folderPath = folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName
     WIKI.logger.debug(`Deleting folder ${folder.id} at path ${folderPath}...`)
     WIKI.logger.debug(`Deleting folder ${folder.id} at path ${folderPath}...`)

+ 8 - 8
server/models/users.mjs

@@ -451,7 +451,7 @@ export class User extends Model {
       })
       })
 
 
       if (strategyId !== expectedStrategyId) {
       if (strategyId !== expectedStrategyId) {
-        throw new Error('ERR_UNEXPECTED_STRATEGY_ID')
+        throw new Error('ERR_INVALID_STRATEGY')
       }
       }
 
 
       if (user) {
       if (user) {
@@ -461,11 +461,11 @@ export class User extends Model {
           }
           }
           return WIKI.db.users.afterLoginChecks(user, strategyId, context, { siteId, skipTFA: true })
           return WIKI.db.users.afterLoginChecks(user, strategyId, context, { siteId, skipTFA: true })
         } else {
         } else {
-          throw new Error('ERR_INCORRECT_TFA_TOKEN')
+          throw new Error('ERR_TFA_INCORRECT_TOKEN')
         }
         }
       }
       }
     }
     }
-    throw new Error('ERR_INVALID_TFA_REQUEST')
+    throw new Error('ERR_TFA_INVALID_REQUEST')
   }
   }
 
 
   /**
   /**
@@ -481,7 +481,7 @@ export class User extends Model {
     })
     })
 
 
     if (strategyId !== expectedStrategyId) {
     if (strategyId !== expectedStrategyId) {
-      throw new Error('ERR_UNEXPECTED_STRATEGY_ID')
+      throw new Error('ERR_INVALID_STRATEGY')
     }
     }
 
 
     if (user) {
     if (user) {
@@ -503,12 +503,12 @@ export class User extends Model {
   static async changePassword ({ strategyId, siteId, currentPassword, newPassword }, context) {
   static async changePassword ({ strategyId, siteId, currentPassword, newPassword }, context) {
     const userId = context.req.user?.id
     const userId = context.req.user?.id
     if (!userId) {
     if (!userId) {
-      throw new Error('ERR_USER_NOT_AUTHENTICATED')
+      throw new Error('ERR_NOT_AUTHENTICATED')
     }
     }
 
 
     const user = await WIKI.db.users.query().findById(userId)
     const user = await WIKI.db.users.query().findById(userId)
     if (!user) {
     if (!user) {
-      throw new Error('ERR_USER_NOT_FOUND')
+      throw new Error('ERR_INVALID_USER')
     }
     }
 
 
     if (!newPassword || newPassword.length < 8) {
     if (!newPassword || newPassword.length < 8) {
@@ -516,7 +516,7 @@ export class User extends Model {
     }
     }
 
 
     if (!user.auth[strategyId]?.password) {
     if (!user.auth[strategyId]?.password) {
-      throw new Error('ERR_UNEXPECTED_STRATEGY_ID')
+      throw new Error('ERR_INVALID_STRATEGY')
     }
     }
 
 
     if (await bcrypt.compare(currentPassword, user.auth[strategyId].password) !== true) {
     if (await bcrypt.compare(currentPassword, user.auth[strategyId].password) !== true) {
@@ -627,7 +627,7 @@ export class User extends Model {
     // Check if email already exists
     // Check if email already exists
     const usr = await WIKI.db.users.query().findOne({ email })
     const usr = await WIKI.db.users.query().findOne({ email })
     if (usr) {
     if (usr) {
-      throw new Error('ERR_ACCOUNT_ALREADY_EXIST')
+      throw new Error('ERR_DUPLICATE_ACCOUNT_EMAIL')
     }
     }
 
 
     WIKI.logger.debug(`Creating new user account for ${email}...`)
     WIKI.logger.debug(`Creating new user account for ${email}...`)

+ 3 - 3
server/modules/authentication/local/authentication.mjs

@@ -21,9 +21,9 @@ export default {
           if (user) {
           if (user) {
             const authStrategyData = user.auth[strategyId]
             const authStrategyData = user.auth[strategyId]
             if (!authStrategyData) {
             if (!authStrategyData) {
-              throw new Error('ERR_INVALID_STRATEGY_ID')
+              throw new Error('ERR_INVALID_STRATEGY')
             } else if (await bcrypt.compare(uPassword, authStrategyData.password) !== true) {
             } else if (await bcrypt.compare(uPassword, authStrategyData.password) !== true) {
-              throw new Error('ERR_AUTH_FAILED')
+              throw new Error('ERR_LOGIN_FAILED')
             } else if (!user.isActive) {
             } else if (!user.isActive) {
               throw new Error('ERR_INACTIVE_USER')
               throw new Error('ERR_INACTIVE_USER')
             } else if (authStrategyData.restrictLogin) {
             } else if (authStrategyData.restrictLogin) {
@@ -34,7 +34,7 @@ export default {
               done(null, user)
               done(null, user)
             }
             }
           } else {
           } else {
-            throw new Error('ERR_AUTH_FAILED')
+            throw new Error('ERR_LOGIN_FAILED')
           }
           }
         } catch (err) {
         } catch (err) {
           done(err, null)
           done(err, null)

+ 1 - 0
ux/public/_assets/icons/fluent-tag.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 48 48" width="96px" height="96px"><linearGradient id="NyTLzsOvu2hiH2q16GFlAa" x1="-5.326" x2="17.563" y1="96.186" y2="50.024" gradientTransform="translate(20.942 -50.558)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#0077d2"/><stop offset="1" stop-color="#0b59a2"/></linearGradient><path fill="url(#NyTLzsOvu2hiH2q16GFlAa)" d="M25.524,9.068L9.455,25.137c-1.435,1.435-1.435,3.761,0,5.196l11.212,11.212	c1.435,1.435,3.761,1.435,5.196,0l16.07-16.07C42.616,24.792,43,23.865,43,22.898V11.646C43,9.641,41.359,8,39.354,8H28.103	C27.135,8,26.208,8.384,25.524,9.068z"/><linearGradient id="NyTLzsOvu2hiH2q16GFlAb" x1="10.819" x2="34.706" y1="5.309" y2="29.196" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#32bdef"/><stop offset="1" stop-color="#1ea2e4"/></linearGradient><path fill="url(#NyTLzsOvu2hiH2q16GFlAb)" d="M21.837,6.049L6.057,21.83c-1.409,1.409-1.409,3.694,0,5.103l11.011,11.011	c1.409,1.409,3.694,1.409,5.103,0l15.781-15.781C38.623,21.491,39,20.58,39,19.63V8.581C39,6.611,37.389,5,35.419,5H24.37	C23.42,5,22.509,5.377,21.837,6.049z M33.629,12.351c-0.985,0-1.79-0.806-1.79-1.79s0.806-1.79,1.79-1.79s1.79,0.806,1.79,1.79	S34.614,12.351,33.629,12.351"/></svg>

+ 4 - 0
ux/src/layouts/AdminLayout.vue

@@ -125,6 +125,10 @@ q-layout.admin(view='hHh Lpr lff')
             q-item-section(side)
             q-item-section(side)
               //- TODO: Reflect site storage status
               //- TODO: Reflect site storage status
               status-light(:color='true ? `positive` : `warning`', :pulse='false')
               status-light(:color='true ? `positive` : `warning`', :pulse='false')
+          q-item(:to='`/_admin/` + adminStore.currentSiteId + `/tags`', v-ripple, active-class='bg-primary text-white', disabled, v-if='flagsStore.experimental && (userStore.can(`manage:sites`))')
+            q-item-section(avatar)
+              q-icon(name='img:/_assets/icons/fluent-tag.svg')
+            q-item-section {{ t('admin.tags.title') }}
           q-item(:to='`/_admin/` + adminStore.currentSiteId + `/theme`', v-ripple, active-class='bg-primary text-white', v-if='userStore.can(`manage:sites`) || userStore.can(`manage:theme`)')
           q-item(:to='`/_admin/` + adminStore.currentSiteId + `/theme`', v-ripple, active-class='bg-primary text-white', v-if='userStore.can(`manage:sites`) || userStore.can(`manage:theme`)')
             q-item-section(avatar)
             q-item-section(avatar)
               q-icon(name='img:/_assets/icons/fluent-paint-roller.svg')
               q-icon(name='img:/_assets/icons/fluent-paint-roller.svg')

+ 22 - 4
ux/src/pages/AdminDashboard.vue

@@ -39,7 +39,7 @@ q-page.admin-dashboard
           img(src='/_assets/icons/fluent-people.svg')
           img(src='/_assets/icons/fluent-people.svg')
           div
           div
             strong {{ t('admin.groups.title') }}
             strong {{ t('admin.groups.title') }}
-            small.text-positive {{adminStore.info.groupsTotal}}
+            span {{adminStore.info.groupsTotal}}
         q-separator
         q-separator
         q-card-actions(align='right')
         q-card-actions(align='right')
           q-btn(
           q-btn(
@@ -85,6 +85,23 @@ q-page.admin-dashboard
             :disable='!userStore.can(`manage:users`)'
             :disable='!userStore.can(`manage:users`)'
             to='/_admin/users'
             to='/_admin/users'
             )
             )
+    .col-12.col-sm-6.col-lg-3
+      q-card
+        q-card-section.admin-dashboard-card
+          img(src='/_assets/icons/fluent-tag.svg')
+          div
+            strong {{ t('admin.tags.title') }}
+            span {{adminStore.info.tagsTotal}}
+        q-separator
+        q-card-actions(align='right')
+          q-btn(
+            flat
+            color='primary'
+            icon='las la-tags'
+            :label='t(`common.actions.manage`)'
+            :disable='!userStore.can(`manage:sites`)'
+            :to='`/_admin/` + adminStore.currentSiteId + `/tags`'
+            )
     .col-12.col-sm-6.col-lg-3
     .col-12.col-sm-6.col-lg-3
       q-card
       q-card
         q-card-section.admin-dashboard-card
         q-card-section.admin-dashboard-card
@@ -102,6 +119,10 @@ q-page.admin-dashboard
             :disable='!userStore.can(`manage:sites`)'
             :disable='!userStore.can(`manage:sites`)'
             :to='`/_admin/` + adminStore.currentSiteId + `/analytics`'
             :to='`/_admin/` + adminStore.currentSiteId + `/analytics`'
             )
             )
+    .col-12.col-lg-9
+      q-card
+        q-card-section ---
+
     .col-12
     .col-12
       q-banner.bg-positive.text-white(
       q-banner.bg-positive.text-white(
         :class='adminStore.isVersionLatest ? `bg-positive` : `bg-warning`'
         :class='adminStore.isVersionLatest ? `bg-positive` : `bg-warning`'
@@ -123,9 +144,6 @@ q-page.admin-dashboard
             :label='t(`admin.system.title`)'
             :label='t(`admin.system.title`)'
             to='/_admin/system'
             to='/_admin/system'
             )
             )
-    .col-12
-      q-card
-        q-card-section ---
 
 
 //- v-container(fluid, grid-list-lg)
 //- v-container(fluid, grid-list-lg)
 //-   v-layout(row, wrap)
 //-   v-layout(row, wrap)

+ 5 - 0
ux/src/stores/admin.js

@@ -13,6 +13,7 @@ export const useAdminStore = defineStore('admin', {
       latestVersion: 'n/a',
       latestVersion: 'n/a',
       groupsTotal: 0,
       groupsTotal: 0,
       pagesTotal: 0,
       pagesTotal: 0,
+      tagsTotal: 0,
       usersTotal: 0,
       usersTotal: 0,
       loginsPastDay: 0,
       loginsPastDay: 0,
       isApiEnabled: false,
       isApiEnabled: false,
@@ -57,7 +58,9 @@ export const useAdminStore = defineStore('admin', {
             apiState
             apiState
             systemInfo {
             systemInfo {
               groupsTotal
               groupsTotal
+              tagsTotal
               usersTotal
               usersTotal
+              loginsPastDay
               currentVersion
               currentVersion
               latestVersion
               latestVersion
               isMailConfigured
               isMailConfigured
@@ -68,7 +71,9 @@ export const useAdminStore = defineStore('admin', {
         fetchPolicy: 'network-only'
         fetchPolicy: 'network-only'
       })
       })
       this.info.groupsTotal = clone(resp?.data?.systemInfo?.groupsTotal ?? 0)
       this.info.groupsTotal = clone(resp?.data?.systemInfo?.groupsTotal ?? 0)
+      this.info.tagsTotal = clone(resp?.data?.systemInfo?.tagsTotal ?? 0)
       this.info.usersTotal = clone(resp?.data?.systemInfo?.usersTotal ?? 0)
       this.info.usersTotal = clone(resp?.data?.systemInfo?.usersTotal ?? 0)
+      this.info.loginsPastDay = clone(resp?.data?.systemInfo?.loginsPastDay ?? 0)
       this.info.currentVersion = clone(resp?.data?.systemInfo?.currentVersion ?? 'n/a')
       this.info.currentVersion = clone(resp?.data?.systemInfo?.currentVersion ?? 'n/a')
       this.info.latestVersion = clone(resp?.data?.systemInfo?.latestVersion ?? 'n/a')
       this.info.latestVersion = clone(resp?.data?.systemInfo?.latestVersion ?? 'n/a')
       this.info.isApiEnabled = clone(resp?.data?.apiState ?? false)
       this.info.isApiEnabled = clone(resp?.data?.apiState ?? false)