diff --git a/server/core/auth.mjs b/server/core/auth.mjs index d889d21f..10440a38 100644 --- a/server/core/auth.mjs +++ b/server/core/auth.mjs @@ -105,6 +105,8 @@ export default { * @param {Express Next Callback} next */ authenticate (req, res, next) { + req.isAuthenticated = false + WIKI.auth.passport.authenticate('jwt', { session: false }, async (err, user, info) => { if (err) { return next() } let mustRevalidate = false @@ -170,6 +172,7 @@ export default { WIKI.auth.guest.cacheExpiration = DateTime.utc().plus({ minutes: 1 }) } req.user = WIKI.auth.guest + req.isAuthenticated = false return next() } @@ -203,6 +206,7 @@ export default { // JWT is valid req.logIn(user, { session: false }, (errc) => { if (errc) { return next(errc) } + req.isAuthenticated = true next() }) })(req, res, next) @@ -223,7 +227,7 @@ export default { return true } - // Check Global Permissions + // Check Permissions if (_.intersection(userPermissions, permissions).length < 1) { return false } diff --git a/server/graph/resolvers/authentication.mjs b/server/graph/resolvers/authentication.mjs index 9e346487..aa06cf4e 100644 --- a/server/graph/resolvers/authentication.mjs +++ b/server/graph/resolvers/authentication.mjs @@ -17,6 +17,10 @@ export default { * List of API Keys */ 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']) return keys.map(k => ({ id: k.id, @@ -31,7 +35,11 @@ export default { /** * 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 }, /** @@ -82,6 +90,10 @@ export default { */ async createApiKey (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:api'])) { + throw new Error('ERR_FORBIDDEN') + } + const key = await WIKI.db.apiKeys.createNewKey(args) await WIKI.auth.reloadApiKeys() WIKI.events.outbound.emit('reloadApiKeys') @@ -136,7 +148,7 @@ export default { try { const userId = context.req.user?.id if (!userId) { - throw new Error('ERR_USER_NOT_AUTHENTICATED') + throw new Error('ERR_NOT_AUTHENTICATED') } const usr = await WIKI.db.users.query().findById(userId) @@ -182,7 +194,7 @@ export default { try { const userId = context.req.user?.id if (!userId) { - throw new Error('ERR_USER_NOT_AUTHENTICATED') + throw new Error('ERR_NOT_AUTHENTICATED') } const usr = await WIKI.db.users.query().findById(userId) @@ -224,7 +236,7 @@ export default { try { const userId = context.req.user?.id if (!userId) { - throw new Error('ERR_USER_NOT_AUTHENTICATED') + throw new Error('ERR_NOT_AUTHENTICATED') } const usr = await WIKI.db.users.query().findById(userId) @@ -283,7 +295,7 @@ export default { try { const userId = context.req.user?.id if (!userId) { - throw new Error('ERR_USER_NOT_AUTHENTICATED') + throw new Error('ERR_NOT_AUTHENTICATED') } const usr = await WIKI.db.users.query().findById(userId) @@ -346,7 +358,7 @@ export default { try { const userId = context.req.user?.id if (!userId) { - throw new Error('ERR_USER_NOT_AUTHENTICATED') + throw new Error('ERR_NOT_AUTHENTICATED') } const usr = await WIKI.db.users.query().findById(userId) @@ -584,7 +596,7 @@ export default { */ async revokeApiKey (obj, args, context) { try { - if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:api'])) { throw new Error('ERR_FORBIDDEN') } diff --git a/server/graph/resolvers/block.mjs b/server/graph/resolvers/block.mjs index a1408f99..48bd91e3 100644 --- a/server/graph/resolvers/block.mjs +++ b/server/graph/resolvers/block.mjs @@ -11,6 +11,9 @@ export default { Mutation: { async setBlocksState(obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:blocks'])) { + throw new Error('ERR_FORBIDDEN') + } // TODO: update blocks state return { operation: generateSuccess('Blocks state updated successfully') diff --git a/server/graph/resolvers/hooks.mjs b/server/graph/resolvers/hooks.mjs index 85333f6e..21cd99b6 100644 --- a/server/graph/resolvers/hooks.mjs +++ b/server/graph/resolvers/hooks.mjs @@ -3,10 +3,18 @@ import _ from 'lodash-es' export default { 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') }, - 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) } }, @@ -14,8 +22,12 @@ export default { /** * CREATE HOOK */ - async createHook (obj, args) { + async createHook (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['write:webhooks', 'manage:webhooks'])) { + throw new Error('ERR_FORBIDDEN') + } + // -> Validate inputs if (!args.name || args.name.length < 1) { throw new WIKI.Error.Custom('HookCreateInvalidName', 'Invalid Hook Name') @@ -41,8 +53,12 @@ export default { /** * UPDATE HOOK */ - async updateHook (obj, args) { + async updateHook (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:webhooks'])) { + throw new Error('ERR_FORBIDDEN') + } + // -> Load hook const hook = await WIKI.db.hooks.query().findById(args.id) if (!hook) { @@ -72,8 +88,12 @@ export default { /** * DELETE HOOK */ - async deleteHook (obj, args) { + async deleteHook (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:webhooks'])) { + throw new Error('ERR_FORBIDDEN') + } + await WIKI.db.hooks.deleteHook(args.id) WIKI.logger.debug(`Hook ${args.id} deleted successfully.`) return { diff --git a/server/graph/resolvers/localization.mjs b/server/graph/resolvers/localization.mjs index 92a08459..49f1d978 100644 --- a/server/graph/resolvers/localization.mjs +++ b/server/graph/resolvers/localization.mjs @@ -1,6 +1,3 @@ -import { generateError, generateSuccess } from '../../helpers/graph.mjs' -import _ from 'lodash-es' - export default { Query: { async locales(obj, args, context, info) { @@ -9,45 +6,5 @@ export default { localeStrings (obj, args, context, info) { 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) - } - } } } diff --git a/server/graph/resolvers/mail.mjs b/server/graph/resolvers/mail.mjs index fe92c9c7..25bec4ba 100644 --- a/server/graph/resolvers/mail.mjs +++ b/server/graph/resolvers/mail.mjs @@ -3,7 +3,11 @@ import { generateError, generateSuccess } from '../../helpers/graph.mjs' export default { 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 { ...WIKI.config.mail, pass: WIKI.config.mail.pass.length > 0 ? '********' : '' @@ -13,6 +17,10 @@ export default { Mutation: { async sendMailTest(obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) { + throw new Error('ERR_FORBIDDEN') + } + if (_.isEmpty(args.recipientEmail) || args.recipientEmail.length < 6) { throw new WIKI.Error.MailInvalidRecipient() } @@ -36,6 +44,10 @@ export default { }, async updateMailConfig(obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) { + throw new Error('ERR_FORBIDDEN') + } + WIKI.config.mail = { senderName: args.senderName, senderEmail: args.senderEmail, diff --git a/server/graph/resolvers/page.mjs b/server/graph/resolvers/page.mjs index 4d5f3a86..163a9118 100644 --- a/server/graph/resolvers/page.mjs +++ b/server/graph/resolvers/page.mjs @@ -1,6 +1,6 @@ import _ from 'lodash-es' import { generateError, generateSuccess } from '../../helpers/graph.mjs' -import { parsePath }from '../../helpers/page.mjs' +import { parsePath } from '../../helpers/page.mjs' import tsquery from 'pg-tsquery' const tsq = tsquery() @@ -247,12 +247,19 @@ export default { siteId: args.siteId }) 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 { throw new Error('ERR_PAGE_NOT_FOUND') @@ -265,17 +272,17 @@ export default { async pathFromAlias (obj, args, context, info) { const alias = args.alias?.trim() if (!alias) { - throw new Error('ERR_ALIAS_MISSING') + throw new Error('ERR_PAGE_ALIAS_MISSING') } 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({ alias: args.alias, siteId: args.siteId }).select('id', 'path', 'locale') if (!page) { - throw new Error('ERR_ALIAS_NOT_FOUND') + throw new Error('ERR_PAGE_ALIAS_NOT_FOUND') } return { id: page.id, @@ -287,7 +294,7 @@ export default { * FETCH TAGS */ 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') // TODO: check permissions return tags @@ -670,19 +677,29 @@ export default { } }, 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 { - 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) { diff --git a/server/graph/resolvers/site.mjs b/server/graph/resolvers/site.mjs index 9bdbaeb4..4fd827db 100644 --- a/server/graph/resolvers/site.mjs +++ b/server/graph/resolvers/site.mjs @@ -49,8 +49,12 @@ export default { /** * CREATE SITE */ - async createSite (obj, args) { + async createSite (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['write:sites', 'manage:sites'])) { + throw new Error('ERR_FORBIDDEN') + } + // -> Validate inputs if (!args.hostname || args.hostname.length < 1 || !/^(\\*)|([a-z0-9\-.:]+)$/.test(args.hostname)) { throw new WIKI.Error.Custom('SiteCreateInvalidHostname', 'Invalid Site Hostname') @@ -83,8 +87,12 @@ export default { /** * UPDATE SITE */ - async updateSite (obj, args) { + async updateSite (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) { + throw new Error('ERR_FORBIDDEN') + } + // -> Load site const site = await WIKI.db.sites.query().findById(args.id) if (!site) { @@ -127,8 +135,12 @@ export default { /** * DELETE SITE */ - async deleteSite (obj, args) { + async deleteSite (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) { + throw new Error('ERR_FORBIDDEN') + } + // -> Ensure site isn't last one const sitesCount = await WIKI.db.sites.query().count('id').first() if (sitesCount?.count && _.toNumber(sitesCount?.count) <= 1) { @@ -149,6 +161,10 @@ export default { */ async uploadSiteLogo (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) { + throw new Error('ERR_FORBIDDEN') + } + const { filename, mimetype, createReadStream } = await args.image WIKI.logger.info(`Processing site logo ${filename} of type ${mimetype}...`) if (!WIKI.extensions.ext.sharp.isInstalled) { @@ -208,6 +224,10 @@ export default { */ async uploadSiteFavicon (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) { + throw new Error('ERR_FORBIDDEN') + } + const { filename, mimetype, createReadStream } = await args.image WIKI.logger.info(`Processing site favicon ${filename} of type ${mimetype}...`) if (!WIKI.extensions.ext.sharp.isInstalled) { @@ -268,6 +288,10 @@ export default { */ async uploadSiteLoginBg (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) { + throw new Error('ERR_FORBIDDEN') + } + const { filename, mimetype, createReadStream } = await args.image WIKI.logger.info(`Processing site login bg ${filename} of type ${mimetype}...`) if (!WIKI.extensions.ext.sharp.isInstalled) { diff --git a/server/graph/resolvers/system.mjs b/server/graph/resolvers/system.mjs index 49401850..4fd7d898 100644 --- a/server/graph/resolvers/system.mjs +++ b/server/graph/resolvers/system.mjs @@ -12,18 +12,44 @@ const getos = util.promisify(getosSync) export default { Query: { + /** + * System Flags + */ systemFlags () { 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'])) for (const ext of exts) { ext.isCompatible = await WIKI.extensions.ext[ext.key].isCompatible() } 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') .select([ 'usename', @@ -56,10 +82,24 @@ export default { } 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 }, - 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 ? await WIKI.db.knex('jobHistory').whereIn('state', args.states.map(s => s.toLowerCase())).orderBy('startedAt', 'desc') : await WIKI.db.knex('jobHistory').orderBy('startedAt', 'desc') @@ -68,16 +108,37 @@ export default { 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') }, - 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([ { column: 'waitUntil', order: 'asc', nulls: 'first' }, { 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 { ...WIKI.config.search, dictOverrides: JSON.stringify(WIKI.config.search.dictOverrides, null, 2) @@ -86,8 +147,13 @@ export default { }, Mutation: { async cancelJob (obj, args, context) { - WIKI.logger.info(`Admin requested cancelling job ${args.id}...`) 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') .where('id', args.id) .del() @@ -100,12 +166,18 @@ export default { operation: generateSuccess('Cancelled job successfully.') } } catch (err) { - WIKI.logger.warn(err) + if (err.message !== 'ERR_FORBIDDEN') { + WIKI.logger.warn(err) + } return generateError(err) } }, async checkForUpdates (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['read:dashboard', 'manage:system'])) { + throw new Error('ERR_FORBIDDEN') + } + const renderJob = await WIKI.scheduler.addJob({ task: 'checkVersion', maxRetries: 0, @@ -119,15 +191,28 @@ export default { latestDate: WIKI.config.update.versionDate } } catch (err) { - WIKI.logger.warn(err) + if (err.message !== 'ERR_FORBIDDEN') { + WIKI.logger.warn(err) + } return generateError(err) } }, 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) { @@ -143,6 +228,10 @@ export default { }, async rebuildSearchIndex (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) { + throw new Error('ERR_FORBIDDEN') + } + await WIKI.scheduler.addJob({ task: 'rebuildSearchIndex', maxRetries: 0 @@ -155,8 +244,13 @@ export default { } }, async retryJob (obj, args, context) { - WIKI.logger.info(`Admin requested rescheduling of job ${args.id}...`) 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') .where('id', args.id) .first() @@ -188,34 +282,61 @@ export default { } }, 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) { - 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) { - 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() return _.toSafeInteger(total.total) }, + async tagsTotal () { + const total = await WIKI.db.tags.query().count('* as total').first() + return _.toSafeInteger(total.total) + }, async usersTotal () { const total = await WIKI.db.users.query().count('* as total').first() 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) } } diff --git a/server/graph/resolvers/tree.mjs b/server/graph/resolvers/tree.mjs index 21654fce..3631a2f5 100644 --- a/server/graph/resolvers/tree.mjs +++ b/server/graph/resolvers/tree.mjs @@ -133,7 +133,7 @@ export default { .first() if (!folder) { - throw new Error('ERR_FOLDER_NOT_EXIST') + throw new Error('ERR_INVALID_FOLDER') } return { @@ -158,7 +158,7 @@ export default { .first() if (!folder) { - throw new Error('ERR_FOLDER_NOT_EXIST') + throw new Error('ERR_INVALID_FOLDER') } return { diff --git a/server/graph/resolvers/user.mjs b/server/graph/resolvers/user.mjs index 9271af79..d9ae8332 100644 --- a/server/graph/resolvers/user.mjs +++ b/server/graph/resolvers/user.mjs @@ -3,6 +3,7 @@ import _, { isNil } from 'lodash-es' import path from 'node:path' import fs from 'fs-extra' import { DateTime } from 'luxon' +import bcrypt from 'bcryptjs' export default { Query: { @@ -10,6 +11,10 @@ export default { * FETCH ALL USERS */ 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 let limit = args.pageSize ?? 20 if (limit < 1 || limit > 1000) { @@ -39,6 +44,12 @@ export default { * FETCH A SINGLE USER */ 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) if (!usr) { @@ -69,31 +80,20 @@ export default { 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) { + if (!WIKI.auth.checkAccess(context.req.user, ['read:users', 'write:users', 'manage:users'])) { + throw new Error('ERR_FORBIDDEN') + } + return WIKI.config.userDefaults }, 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() .select('id', 'name', 'lastLoginAt') .whereNotNull('lastLoginAt') @@ -114,8 +114,12 @@ export default { } }, Mutation: { - async createUser (obj, args) { + async createUser (obj, args, context) { 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 }) return { @@ -125,8 +129,12 @@ export default { return generateError(err) } }, - async deleteUser (obj, args) { + async deleteUser (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:users'])) { + throw new Error('ERR_FORBIDDEN') + } + if (args.id <= 2) { throw new WIKI.Error.UserDeleteProtected() } @@ -146,8 +154,12 @@ export default { } } }, - async updateUser (obj, args) { + async updateUser (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:users'])) { + throw new Error('ERR_FORBIDDEN') + } + await WIKI.db.users.updateUser(args.id, args.patch) return { @@ -157,8 +169,12 @@ export default { return generateError(err) } }, - async verifyUser (obj, args) { + async verifyUser (obj, args, context) { 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) return { @@ -168,8 +184,12 @@ export default { return generateError(err) } }, - async activateUser (obj, args) { + async activateUser (obj, args, context) { 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) return { @@ -179,8 +199,12 @@ export default { return generateError(err) } }, - async deactivateUser (obj, args) { + async deactivateUser (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:users'])) { + throw new Error('ERR_FORBIDDEN') + } + if (args.id <= 2) { throw new Error('Cannot deactivate system accounts.') } @@ -196,8 +220,12 @@ export default { return generateError(err) } }, - async enableUserTFA (obj, args) { + async enableUserTFA (obj, args, context) { 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) return { @@ -207,8 +235,12 @@ export default { return generateError(err) } }, - async disableUserTFA (obj, args) { + async disableUserTFA (obj, args, context) { 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) return { @@ -220,13 +252,17 @@ export default { }, async changeUserPassword (obj, args, context) { try { + if (!WIKI.auth.checkAccess(context.req.user, ['manage:users'])) { + throw new Error('ERR_FORBIDDEN') + } + if (args.newPassword?.length < 8) { throw new Error('ERR_PASSWORD_TOO_SHORT') } const usr = await WIKI.db.users.query().findById(args.id) if (!usr) { - throw new Error('ERR_USER_NOT_FOUND') + throw new Error('ERR_INVALID_USER') } const localAuth = await WIKI.db.authentication.getStrategy('local') @@ -249,22 +285,22 @@ export default { async updateProfile (obj, args, context) { try { 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) if (!usr.isActive) { - throw new WIKI.Error.AuthAccountBanned() + throw new Error('ERR_INACTIVE_USER') } 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)) { - throw new WIKI.Error.InputInvalid() + throw new Error('ERR_INVALID_INPUT') } 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({ @@ -292,47 +328,19 @@ export default { 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 */ - async uploadUserAvatar (obj, args) { + async uploadUserAvatar (obj, args, context) { 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 lowercaseFilename = filename.toLowerCase() WIKI.logger.debug(`Processing user ${args.id} avatar ${lowercaseFilename} of type ${mimetype}...`) @@ -358,7 +366,7 @@ export default { height: 180 }) // -> 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 const imgBuffer = await fs.readFile(destPath) await WIKI.db.knex('userAvatars').insert({ @@ -377,10 +385,18 @@ export default { /** * CLEAR USER AVATAR */ - async clearUserAvatar (obj, args) { + async clearUserAvatar (obj, args, context) { 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...`) - 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() WIKI.logger.debug(`Cleared user ${args.id} avatar successfully.`) return { @@ -395,19 +411,27 @@ export default { * UPDATE USER DEFAULTS */ 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: { - async auth (usr) { + async auth (usr, args, context) { const authStrategies = await WIKI.db.authentication.getStrategies({ enabledOnly: true }) return _.transform(usr.auth, (result, value, key) => { const authStrategy = _.find(authStrategies, ['id', key]) @@ -432,14 +456,4 @@ export default { 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) - // } - // } } diff --git a/server/graph/schemas/localization.graphql b/server/graph/schemas/localization.graphql index dab45bf9..0c690151 100644 --- a/server/graph/schemas/localization.graphql +++ b/server/graph/schemas/localization.graphql @@ -7,19 +7,6 @@ extend type Query { localeStrings(locale: String!): JSON } -extend type Mutation { - downloadLocale( - locale: String! - ): DefaultResponse - - updateLocale( - locale: String! - autoUpdate: Boolean! - namespacing: Boolean! - namespaces: [String]! - ): DefaultResponse -} - # ----------------------------------------------- # TYPES # ----------------------------------------------- diff --git a/server/graph/schemas/system.graphql b/server/graph/schemas/system.graphql index 32564b1b..4da13974 100644 --- a/server/graph/schemas/system.graphql +++ b/server/graph/schemas/system.graphql @@ -86,6 +86,7 @@ type SystemInfo { isSchedulerHealthy: Boolean latestVersion: String latestVersionReleaseDate: Date + loginsPastDay: Int nodeVersion: String operatingSystem: String pagesTotal: Int diff --git a/server/models/pages.mjs b/server/models/pages.mjs index 61822352..2b01b7a5 100644 --- a/server/models/pages.mjs +++ b/server/models/pages.mjs @@ -227,7 +227,7 @@ export class Page extends Model { static async createPage(opts) { // -> Validate site if (!WIKI.sites[opts.siteId]) { - throw new Error('ERR_INVALID_SITE_ID') + throw new Error('ERR_INVALID_SITE') } // -> Remove trailing slash diff --git a/server/models/tree.mjs b/server/models/tree.mjs index 005f261d..6970a88e 100644 --- a/server/models/tree.mjs +++ b/server/models/tree.mjs @@ -77,7 +77,7 @@ export class Tree extends Model { if (id) { const parent = await WIKI.db.knex('tree').where('id', id).first() if (!parent) { - throw new Error('ERR_NONEXISTING_FOLDER_ID') + throw new Error('ERR_INVALID_FOLDER') } return parent } else { @@ -105,7 +105,7 @@ export class Tree extends Model { siteId }) } else { - throw new Error('ERR_NONEXISTING_FOLDER_PATH') + throw new Error('ERR_INVALID_FOLDER') } } } @@ -150,7 +150,7 @@ export class Tree extends Model { siteId, tags, meta, - navigationId: siteId, + navigationId: siteId }).returning('*') return pageEntry[0] @@ -215,12 +215,12 @@ export class Tree extends Model { static async createFolder ({ parentId, parentPath, pathName, title, locale, siteId }) { // Validate path name if (!rePathName.test(pathName)) { - throw new Error('ERR_INVALID_PATH_NAME') + throw new Error('ERR_INVALID_PATH') } // Validate title if (!reTitle.test(title)) { - throw new Error('ERR_INVALID_TITLE') + throw new Error('ERR_FOLDER_TITLE_INVALID') } parentPath = encodeTreePath(parentPath) @@ -236,7 +236,7 @@ export class Tree extends Model { if (parentId) { parent = await WIKI.db.knex('tree').where('id', parentId).first() 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 } else if (parentPath) { @@ -254,7 +254,7 @@ export class Tree extends Model { type: 'folder' }).first() if (existingFolder) { - throw new Error('ERR_FOLDER_ALREADY_EXISTS') + throw new Error('ERR_FOLDER_DUPLICATE') } // Ensure all ancestors exist @@ -338,17 +338,17 @@ export class Tree extends Model { // Get folder const folder = await WIKI.db.knex('tree').where('id', folderId).first() if (!folder) { - throw new Error('ERR_NONEXISTING_FOLDER_ID') + throw new Error('ERR_INVALID_FOLDER') } // Validate path name if (!rePathName.test(pathName)) { - throw new Error('ERR_INVALID_PATH_NAME') + throw new Error('ERR_INVALID_PATH') } // Validate 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}...`) @@ -364,7 +364,7 @@ export class Tree extends Model { type: 'folder' }).first() if (existingFolder) { - throw new Error('ERR_FOLDER_ALREADY_EXISTS') + throw new Error('ERR_FOLDER_DUPLICATE') } // Build new paths @@ -406,7 +406,7 @@ export class Tree extends Model { // Get folder const folder = await WIKI.db.knex('tree').where('id', folderId).first() 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 WIKI.logger.debug(`Deleting folder ${folder.id} at path ${folderPath}...`) diff --git a/server/models/users.mjs b/server/models/users.mjs index b24994f7..e623b932 100644 --- a/server/models/users.mjs +++ b/server/models/users.mjs @@ -451,7 +451,7 @@ export class User extends Model { }) if (strategyId !== expectedStrategyId) { - throw new Error('ERR_UNEXPECTED_STRATEGY_ID') + throw new Error('ERR_INVALID_STRATEGY') } if (user) { @@ -461,11 +461,11 @@ export class User extends Model { } return WIKI.db.users.afterLoginChecks(user, strategyId, context, { siteId, skipTFA: true }) } 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) { - throw new Error('ERR_UNEXPECTED_STRATEGY_ID') + throw new Error('ERR_INVALID_STRATEGY') } if (user) { @@ -503,12 +503,12 @@ export class User extends Model { static async changePassword ({ strategyId, siteId, currentPassword, newPassword }, context) { const userId = context.req.user?.id if (!userId) { - throw new Error('ERR_USER_NOT_AUTHENTICATED') + throw new Error('ERR_NOT_AUTHENTICATED') } const user = await WIKI.db.users.query().findById(userId) if (!user) { - throw new Error('ERR_USER_NOT_FOUND') + throw new Error('ERR_INVALID_USER') } if (!newPassword || newPassword.length < 8) { @@ -516,7 +516,7 @@ export class User extends Model { } 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) { @@ -627,7 +627,7 @@ export class User extends Model { // Check if email already exists const usr = await WIKI.db.users.query().findOne({ email }) 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}...`) diff --git a/server/modules/authentication/local/authentication.mjs b/server/modules/authentication/local/authentication.mjs index 3e41d8f0..71af987e 100644 --- a/server/modules/authentication/local/authentication.mjs +++ b/server/modules/authentication/local/authentication.mjs @@ -21,9 +21,9 @@ export default { if (user) { const authStrategyData = user.auth[strategyId] if (!authStrategyData) { - throw new Error('ERR_INVALID_STRATEGY_ID') + throw new Error('ERR_INVALID_STRATEGY') } 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) { throw new Error('ERR_INACTIVE_USER') } else if (authStrategyData.restrictLogin) { @@ -34,7 +34,7 @@ export default { done(null, user) } } else { - throw new Error('ERR_AUTH_FAILED') + throw new Error('ERR_LOGIN_FAILED') } } catch (err) { done(err, null) diff --git a/ux/public/_assets/icons/fluent-tag.svg b/ux/public/_assets/icons/fluent-tag.svg new file mode 100644 index 00000000..0d873492 --- /dev/null +++ b/ux/public/_assets/icons/fluent-tag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ux/src/layouts/AdminLayout.vue b/ux/src/layouts/AdminLayout.vue index 1c92af41..20fe0344 100644 --- a/ux/src/layouts/AdminLayout.vue +++ b/ux/src/layouts/AdminLayout.vue @@ -125,6 +125,10 @@ q-layout.admin(view='hHh Lpr lff') q-item-section(side) //- TODO: Reflect site storage status 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-section(avatar) q-icon(name='img:/_assets/icons/fluent-paint-roller.svg') diff --git a/ux/src/pages/AdminDashboard.vue b/ux/src/pages/AdminDashboard.vue index 94178130..c6704430 100644 --- a/ux/src/pages/AdminDashboard.vue +++ b/ux/src/pages/AdminDashboard.vue @@ -39,7 +39,7 @@ q-page.admin-dashboard img(src='/_assets/icons/fluent-people.svg') div strong {{ t('admin.groups.title') }} - small.text-positive {{adminStore.info.groupsTotal}} + span {{adminStore.info.groupsTotal}} q-separator q-card-actions(align='right') q-btn( @@ -85,6 +85,23 @@ q-page.admin-dashboard :disable='!userStore.can(`manage: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 q-card q-card-section.admin-dashboard-card @@ -102,6 +119,10 @@ q-page.admin-dashboard :disable='!userStore.can(`manage:sites`)' :to='`/_admin/` + adminStore.currentSiteId + `/analytics`' ) + .col-12.col-lg-9 + q-card + q-card-section --- + .col-12 q-banner.bg-positive.text-white( :class='adminStore.isVersionLatest ? `bg-positive` : `bg-warning`' @@ -123,9 +144,6 @@ q-page.admin-dashboard :label='t(`admin.system.title`)' to='/_admin/system' ) - .col-12 - q-card - q-card-section --- //- v-container(fluid, grid-list-lg) //- v-layout(row, wrap) diff --git a/ux/src/stores/admin.js b/ux/src/stores/admin.js index b83ef80d..0562e52f 100644 --- a/ux/src/stores/admin.js +++ b/ux/src/stores/admin.js @@ -13,6 +13,7 @@ export const useAdminStore = defineStore('admin', { latestVersion: 'n/a', groupsTotal: 0, pagesTotal: 0, + tagsTotal: 0, usersTotal: 0, loginsPastDay: 0, isApiEnabled: false, @@ -57,7 +58,9 @@ export const useAdminStore = defineStore('admin', { apiState systemInfo { groupsTotal + tagsTotal usersTotal + loginsPastDay currentVersion latestVersion isMailConfigured @@ -68,7 +71,9 @@ export const useAdminStore = defineStore('admin', { fetchPolicy: 'network-only' }) 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.loginsPastDay = clone(resp?.data?.systemInfo?.loginsPastDay ?? 0) this.info.currentVersion = clone(resp?.data?.systemInfo?.currentVersion ?? 'n/a') this.info.latestVersion = clone(resp?.data?.systemInfo?.latestVersion ?? 'n/a') this.info.isApiEnabled = clone(resp?.data?.apiState ?? false)