refactor: update schema with new structure / naming

pull/5698/head
NGPixel 3 years ago
parent dfde2e10aa
commit 358ad1fdcd
No known key found for this signature in database
GPG Key ID: 8FDA2F1757F60D63

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save