fix: add permissions to resolvers

pull/6813/head
NGPixel 1 year ago
parent 88197c174c
commit 2a3e1400a7
No known key found for this signature in database
GPG Key ID: B755FB6870B30F63

@ -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
}

@ -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')
}

@ -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')

@ -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 {

@ -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)
}
}
}
}

@ -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,

@ -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,6 +247,10 @@ export default {
siteId: args.siteId
})
if (page) {
if (WIKI.auth.checkAccess(context.req.user, ['read:pages'], {
path: page.path,
locale: page.locale
})) {
return {
...page,
...page.config,
@ -254,6 +258,9 @@ export default {
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 (obj) {
return obj.password ? '********' : ''
password (page) {
return page.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) {

@ -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) {

@ -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) {
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,16 +191,29 @@ export default {
latestDate: WIKI.config.update.versionDate
}
} catch (err) {
if (err.message !== 'ERR_FORBIDDEN') {
WIKI.logger.warn(err)
}
return generateError(err)
}
},
async disconnectWS (obj, args, context) {
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) {
try {
@ -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,6 +282,11 @@ export default {
}
},
async updateSystemFlags (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:system'])) {
throw new Error('ERR_FORBIDDEN')
}
WIKI.config.flags = {
...WIKI.config.flags,
...args.flags
@ -197,8 +296,17 @@ export default {
return {
operation: generateSuccess('System Flags applied successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return generateError(err)
}
},
async updateSystemSearch (obj, args, context) {
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,
@ -209,14 +317,27 @@ export default {
return {
operation: generateSuccess('System Search configuration applied successfully')
}
} catch (err) {
WIKI.logger.warn(err)
return generateError(err)
}
},
async updateSystemSecurity (obj, args, context) {
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)
}
}
},
SystemInfo: {
@ -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)
}
}

@ -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 {

@ -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,6 +411,11 @@ export default {
* UPDATE USER DEFAULTS
*/
async updateUserDefaults (obj, args, context) {
try {
if (!WIKI.auth.checkAccess(context.req.user, ['manage:users'])) {
throw new Error('ERR_FORBIDDEN')
}
WIKI.config.userDefaults = {
timezone: args.timezone,
dateFormat: args.dateFormat,
@ -404,10 +425,13 @@ export default {
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)
// }
// }
}

@ -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
# -----------------------------------------------

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

@ -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

@ -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}...`)

@ -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}...`)

@ -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)

@ -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>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -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')

@ -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)

@ -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)

Loading…
Cancel
Save