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 path = require('path')
const autoload = require('auto-load') const autoload = require('auto-load')
const { makeExecutableSchema } = require('@graphql-tools/schema') 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 { GraphQLUpload } = require('graphql-upload')
const { rateLimitDirectiveTypeDefs, rateLimitDirectiveTransformer } = rateLimitDirective()
/* global WIKI */ /* 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 // Schemas
WIKI.logger.info(`Loading GraphQL Schema...`)
const typeDefs = [ const typeDefs = [
rateLimitDirectiveTypeDefs rateLimitDirectiveTypeDefs
] ]
@ -23,7 +27,11 @@ schemas.forEach(schema => {
// Resolvers // Resolvers
WIKI.logger.info(`Loading GraphQL Resolvers...`)
let resolvers = { let resolvers = {
Date: require('./scalars/date'),
JSON: require('./scalars/json'),
UUID: require('./scalars/uuid'),
Upload: GraphQLUpload Upload: GraphQLUpload
} }
const resolversObj = _.values(autoload(path.join(WIKI.SERVERPATH, 'graph/resolvers'))) const resolversObj = _.values(autoload(path.join(WIKI.SERVERPATH, 'graph/resolvers')))
@ -33,11 +41,14 @@ resolversObj.forEach(resolver => {
// Make executable schema // Make executable schema
WIKI.logger.info(`Compiling GraphQL Schema...`)
let schema = makeExecutableSchema({ let schema = makeExecutableSchema({
typeDefs, typeDefs,
resolvers resolvers
}) })
// Apply schema transforms
schema = rateLimitDirectiveTransformer(schema) schema = rateLimitDirectiveTransformer(schema)
WIKI.logger.info(`GraphQL Schema: [ OK ]`) WIKI.logger.info(`GraphQL Schema: [ OK ]`)

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

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

@ -7,12 +7,6 @@ const graphHelper = require('../../helpers/graph')
module.exports = { module.exports = {
Query: { Query: {
async authentication () { return {} }
},
Mutation: {
async authentication () { return {} }
},
AuthenticationQuery: {
/** /**
* List of API Keys * List of API Keys
*/ */
@ -34,7 +28,7 @@ module.exports = {
apiState () { apiState () {
return WIKI.config.api.isEnabled return WIKI.config.api.isEnabled
}, },
async strategies () { async authStrategies () {
return WIKI.data.authentication.map(stg => ({ return WIKI.data.authentication.map(stg => ({
...stg, ...stg,
isAvailable: stg.isAvailable === true, isAvailable: stg.isAvailable === true,
@ -45,35 +39,35 @@ module.exports = {
}) })
}, []), 'key') }, []), '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 * Create New API Key
*/ */
@ -197,7 +191,7 @@ module.exports = {
/** /**
* Update Authentication Strategies * Update Authentication Strategies
*/ */
async updateStrategies (obj, args, context) { async updateAuthStrategies (obj, args, context) {
try { try {
const previousStrategies = await WIKI.models.authentication.getStrategies() const previousStrategies = await WIKI.models.authentication.getStrategies()
for (const str of args.strategies) { for (const str of args.strategies) {

@ -5,16 +5,10 @@ const graphHelper = require('../../helpers/graph')
module.exports = { module.exports = {
Query: { Query: {
async comments() { return {} }
},
Mutation: {
async comments() { return {} }
},
CommentQuery: {
/** /**
* Fetch list of Comments Providers * Fetch list of Comments Providers
*/ */
async providers(obj, args, context, info) { async commentsProviders(obj, args, context, info) {
const providers = await WIKI.models.commentProviders.getProviders() const providers = await WIKI.models.commentProviders.getProviders()
return providers.map(provider => { return providers.map(provider => {
const providerInfo = _.find(WIKI.data.commentProviders, ['key', provider.key]) || {} const providerInfo = _.find(WIKI.data.commentProviders, ['key', provider.key]) || {}
@ -39,7 +33,7 @@ module.exports = {
/** /**
* Fetch list of comments for a page * 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 }) const page = await WIKI.models.pages.query().select('id').findOne({ localeCode: args.locale, path: args.path })
if (page) { if (page) {
if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], args)) { if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], args)) {
@ -60,7 +54,7 @@ module.exports = {
/** /**
* Fetch a single comment * Fetch a single comment
*/ */
async single (obj, args, context) { async commentById (obj, args, context) {
const cm = await WIKI.data.commentProvider.getCommentById(args.id) const cm = await WIKI.data.commentProvider.getCommentById(args.id)
if (!cm || !cm.pageId) { if (!cm || !cm.pageId) {
throw new WIKI.Error.CommentNotFound() throw new WIKI.Error.CommentNotFound()
@ -86,11 +80,11 @@ module.exports = {
} }
} }
}, },
CommentMutation: { Mutation: {
/** /**
* Create New Comment * Create New Comment
*/ */
async create (obj, args, context) { async createComment (obj, args, context) {
try { try {
const cmId = await WIKI.models.comments.postNewComment({ const cmId = await WIKI.models.comments.postNewComment({
...args, ...args,
@ -108,7 +102,7 @@ module.exports = {
/** /**
* Update an Existing Comment * Update an Existing Comment
*/ */
async update (obj, args, context) { async updateComment (obj, args, context) {
try { try {
const cmRender = await WIKI.models.comments.updateComment({ const cmRender = await WIKI.models.comments.updateComment({
...args, ...args,
@ -126,7 +120,7 @@ module.exports = {
/** /**
* Delete an Existing Comment * Delete an Existing Comment
*/ */
async delete (obj, args, context) { async deleteComment (obj, args, context) {
try { try {
await WIKI.models.comments.deleteComment({ await WIKI.models.comments.deleteComment({
id: args.id, id: args.id,
@ -143,7 +137,7 @@ module.exports = {
/** /**
* Update Comments Providers * Update Comments Providers
*/ */
async updateProviders(obj, args, context) { async updateCommentsProviders(obj, args, context) {
try { try {
for (let provider of args.providers) { for (let provider of args.providers) {
await WIKI.models.commentProviders.query().patch({ 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 = { module.exports = {
Query: { Query: {
async groups () { return {} }
},
Mutation: {
async groups () { return {} }
},
GroupQuery: {
/** /**
* FETCH ALL GROUPS * FETCH ALL GROUPS
*/ */
async list () { async groups () {
return WIKI.models.groups.query().select( return WIKI.models.groups.query().select(
'groups.*', 'groups.*',
WIKI.models.groups.relatedQuery('users').count().as('userCount') WIKI.models.groups.relatedQuery('users').count().as('userCount')
@ -25,15 +19,15 @@ module.exports = {
/** /**
* FETCH A SINGLE GROUP * FETCH A SINGLE GROUP
*/ */
async single(obj, args) { async groupById(obj, args) {
return WIKI.models.groups.query().findById(args.id) return WIKI.models.groups.query().findById(args.id)
} }
}, },
GroupMutation: { Mutation: {
/** /**
* ASSIGN USER TO GROUP * ASSIGN USER TO GROUP
*/ */
async assignUser (obj, args, { req }) { async assignUserToGroup (obj, args, { req }) {
// Check for guest user // Check for guest user
if (args.userId === 2) { if (args.userId === 2) {
throw new gql.GraphQLError('Cannot assign the Guest user to a group.') throw new gql.GraphQLError('Cannot assign the Guest user to a group.')
@ -85,7 +79,7 @@ module.exports = {
/** /**
* CREATE NEW GROUP * CREATE NEW GROUP
*/ */
async create (obj, args, { req }) { async createGroup (obj, args, { req }) {
const group = await WIKI.models.groups.query().insertAndFetch({ const group = await WIKI.models.groups.query().insertAndFetch({
name: args.name, name: args.name,
permissions: JSON.stringify(WIKI.data.groups.defaultPermissions), permissions: JSON.stringify(WIKI.data.groups.defaultPermissions),
@ -102,7 +96,7 @@ module.exports = {
/** /**
* DELETE GROUP * DELETE GROUP
*/ */
async delete (obj, args) { async deleteGroup (obj, args) {
if (args.id === 1 || args.id === 2) { if (args.id === 1 || args.id === 2) {
throw new gql.GraphQLError('Cannot delete this group.') throw new gql.GraphQLError('Cannot delete this group.')
} }
@ -122,7 +116,7 @@ module.exports = {
/** /**
* UNASSIGN USER FROM GROUP * UNASSIGN USER FROM GROUP
*/ */
async unassignUser (obj, args) { async unassignUserFromGroup (obj, args) {
if (args.userId === 2) { if (args.userId === 2) {
throw new gql.GraphQLError('Cannot unassign Guest user') throw new gql.GraphQLError('Cannot unassign Guest user')
} }
@ -149,7 +143,7 @@ module.exports = {
/** /**
* UPDATE GROUP * UPDATE GROUP
*/ */
async update (obj, args, { req }) { async updateGroup (obj, args, { req }) {
// Check for unsafe regex page rules // Check for unsafe regex page rules
if (_.some(args.pageRules, pr => { if (_.some(args.pageRules, pr => {
return pr.match === 'REGEX' && !safeRegex(pr.path) return pr.match === 'REGEX' && !safeRegex(pr.path)

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

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

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

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

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

@ -1,74 +1,10 @@
const _ = require('lodash')
const graphHelper = require('../../helpers/graph') const graphHelper = require('../../helpers/graph')
/* global WIKI */ /* global WIKI */
module.exports = { module.exports = {
Query: {
async search() { return {} }
},
Mutation: { Mutation: {
async search() { return {} } async rebuildSearchIndex (obj, args, context) {
},
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) {
try { try {
await WIKI.data.searchEngine.rebuild() await WIKI.data.searchEngine.rebuild()
return { return {

@ -40,9 +40,7 @@ module.exports = {
hostname: site.hostname, hostname: site.hostname,
isEnabled: site.isEnabled isEnabled: site.isEnabled
} : null } : null
}, }
// LEGACY
async site() { return {} }
}, },
Mutation: { Mutation: {
/** /**
@ -178,118 +176,6 @@ module.exports = {
return { return {
status: graphHelper.generateSuccess('Site favicon uploaded successfully') 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 _ = require('lodash')
const graphHelper = require('../../helpers/graph') const graphHelper = require('../../helpers/graph')
const { v4: uuid } = require('uuid')
/* global WIKI */ /* global WIKI */
module.exports = { module.exports = {
Query: { Query: {
async storage() { return {} } 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 {
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
}, },
Mutation: { assetDelivery: {
async storage() { return {} } 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
}, },
StorageQuery: { sync: {},
async targets(obj, args, context, info) { status: {},
let targets = await WIKI.models.storage.getTargets() setup: {
targets = _.sortBy(targets.map(tgt => { handler: md?.setup?.handler,
const targetInfo = _.find(WIKI.data.storage, ['key', tgt.key]) || {} 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 { return {
...targetInfo, value: o,
...tgt, label: o
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
}) })
}
}
}, {}),
actions: md.actions
}
}), ['title'])
}
},
Mutation: {
async updateStorageTargets (obj, args, context) {
WIKI.logger.debug(`Updating storage targets for site ${args.siteId}...`)
try {
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.')
}
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]
}
}
// -> 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')
} }
}, []), 'key') } catch (err) {
return graphHelper.generateError(err)
} }
}), ['title', 'key'])
return targets
}, },
async status(obj, args, context, info) { async setupStorageTarget (obj, args, context) {
let activeTargets = await WIKI.models.storage.query().where('isEnabled', true) try {
return activeTargets.map(tgt => { const tgt = await WIKI.models.storage.query().findById(args.targetId)
const targetInfo = _.find(WIKI.data.storage, ['key', tgt.key]) || {} 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 { return {
key: tgt.key, status: graphHelper.generateSuccess('Storage target setup step succeeded'),
title: targetInfo.title, state: result
status: _.get(tgt, 'state.status', 'pending'),
message: _.get(tgt, 'state.message', 'Initializing...'),
lastAttempt: _.get(tgt, 'state.lastAttempt', null)
} }
}) } catch (err) {
return graphHelper.generateError(err)
} }
}, },
StorageMutation: { async destroyStorageTargetSetup (obj, args, context) {
async updateTargets(obj, args, context) {
try { try {
let dbTargets = await WIKI.models.storage.getTargets() const tgt = await WIKI.models.storage.query().findById(args.targetId)
for (let tgt of args.targets) { if (!tgt) {
const currentDbTarget = _.find(dbTargets, ['key', tgt.key]) throw new Error('Not storage target matching this ID')
if (!currentDbTarget) {
continue
} }
await WIKI.models.storage.query().patch({ const md = _.find(WIKI.storage.defs, ['key', tgt.module])
isEnabled: tgt.isEnabled, if (!md) {
mode: tgt.mode, throw new Error('No matching storage module installed.')
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
} }
}).where('key', tgt.key) if (!await WIKI.models.storage.ensureModule(md.key)) {
throw new Error('Failed to load storage module. Check logs for details.')
} }
await WIKI.models.storage.initTargets() await WIKI.storage.modules[md.key].setupDestroy(args.targetId)
return { return {
responseResult: graphHelper.generateSuccess('Storage targets updated successfully') status: graphHelper.generateSuccess('Storage target setup configuration destroyed succesfully.')
} }
} catch (err) { } catch (err) {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
async executeAction(obj, args, context) { async executeStorageAction (obj, args, context) {
try { try {
await WIKI.models.storage.executeAction(args.targetKey, args.handler) await WIKI.models.storage.executeAction(args.targetKey, args.handler)
return { return {
responseResult: graphHelper.generateSuccess('Action completed.') status: graphHelper.generateSuccess('Action completed.')
} }
} catch (err) { } catch (err) {
return graphHelper.generateError(err) return graphHelper.generateError(err)

@ -5,257 +5,55 @@ const os = require('os')
const filesize = require('filesize') const filesize = require('filesize')
const path = require('path') const path = require('path')
const fs = require('fs-extra') const fs = require('fs-extra')
const moment = require('moment') const { DateTime } = require('luxon')
const graphHelper = require('../../helpers/graph') 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 */ /* global WIKI */
const dbTypes = {
mysql: 'MySQL',
mariadb: 'MariaDB',
postgres: 'PostgreSQL',
sqlite: 'SQLite',
mssql: 'MS SQL Server'
}
module.exports = { module.exports = {
Query: { Query: {
async system () { return {} } systemFlags () {
},
Mutation: {
async system () { return {} }
},
SystemQuery: {
flags () {
return _.transform(WIKI.config.flags, (result, value, key) => { return _.transform(WIKI.config.flags, (result, value, key) => {
result.push({ key, value }) result.push({ key, value })
}, []) }, [])
}, },
async info () { return {} }, async systemInfo () { return {} },
async extensions () { async systemExtensions () {
const exts = Object.values(WIKI.extensions.ext).map(ext => _.pick(ext, ['key', 'title', 'description', 'isInstalled'])) const exts = Object.values(WIKI.extensions.ext).map(ext => _.pick(ext, ['key', 'title', 'description', 'isInstalled', 'isInstallable']))
for (let ext of exts) { for (const ext of exts) {
ext.isCompatible = await WIKI.extensions.ext[ext.key].isCompatible() ext.isCompatible = await WIKI.extensions.ext[ext.key].isCompatible()
} }
return exts return exts
},
systemSecurity () {
return WIKI.config.security
} }
}, },
SystemMutation: { Mutation: {
async updateFlags (obj, args, context) { async updateSystemFlags (obj, args, context) {
WIKI.config.flags = _.transform(args.flags, (result, row) => { WIKI.config.flags = _.transform(args.flags, (result, row) => {
_.set(result, row.key, row.value) _.set(result, row.key, row.value)
}, {}) }, {})
await WIKI.configSvc.applyFlags() await WIKI.configSvc.applyFlags()
await WIKI.configSvc.saveToDb(['flags']) await WIKI.configSvc.saveToDb(['flags'])
return { return {
responseResult: graphHelper.generateSuccess('System Flags applied successfully') status: 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)
} }
}, },
/** async updateSystemSecurity (obj, args, context) {
* Set HTTPS Redirection State WIKI.config.security = _.defaultsDeep(_.omit(args, ['__typename']), WIKI.config.security)
*/ // TODO: broadcast config update
async setHTTPSRedirection (obj, args, context) { await WIKI.configSvc.saveToDb(['security'])
_.set(WIKI.config, 'server.sslRedir', args.enabled)
await WIKI.configSvc.saveToDb(['server'])
return { return {
responseResult: graphHelper.generateSuccess('HTTP Redirection state set successfully.') status: graphHelper.generateSuccess('System Security configuration applied successfully')
} }
}, },
/** async installExtension (obj, args, context) {
* Renew SSL Certificate
*/
async renewHTTPSCertificate (obj, args, context) {
try { try {
if (!WIKI.config.ssl.enabled) { await WIKI.extensions.ext[args.key].install()
throw new WIKI.Error.SystemSSLDisabled() // TODO: broadcast ext install
} 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 { return {
responseResult: graphHelper.generateSuccess('SSL Certificate renewed successfully.') status: graphHelper.generateSuccess('Extension installed successfully')
}
} }
} catch (err) { } catch (err) {
return graphHelper.generateError(err) return graphHelper.generateError(err)
@ -272,36 +70,11 @@ module.exports = {
currentVersion () { currentVersion () {
return WIKI.version return WIKI.version
}, },
dbType () {
return _.get(dbTypes, WIKI.config.db.type, 'Unknown DB')
},
async dbVersion () { async dbVersion () {
let version = 'Unknown Version' return _.get(WIKI.models, 'knex.client.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
}, },
dbHost () { dbHost () {
if (WIKI.config.db.type === 'sqlite') {
return WIKI.config.db.storage
} else {
return WIKI.config.db.host return WIKI.config.db.host
}
}, },
hostname () { hostname () {
return os.hostname() return os.hostname()
@ -319,7 +92,7 @@ module.exports = {
return WIKI.system.updates.version return WIKI.system.updates.version
}, },
latestVersionReleaseDate () { latestVersionReleaseDate () {
return moment.utc(WIKI.system.updates.releaseDate) return DateTime.fromISO(WIKI.system.updates.releaseDate).toJSDate()
}, },
nodeVersion () { nodeVersion () {
return process.version.substr(1) return process.version.substr(1)
@ -343,10 +116,10 @@ module.exports = {
return filesize(os.totalmem()) return filesize(os.totalmem())
}, },
sslDomain () { 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 () { 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 () { sslProvider () {
return WIKI.config.ssl.enabled ? WIKI.config.ssl.provider : null return WIKI.config.ssl.enabled ? WIKI.config.ssl.provider : null
@ -355,7 +128,7 @@ module.exports = {
return 'OK' return 'OK'
}, },
sslSubscriberEmail () { 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 () { telemetry () {
return WIKI.telemetry.enabled 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 = { module.exports = {
Query: { Query: {
async users() { return {} } /**
}, * FETCH ALL USERS
Mutation: { */
async users() { return {} } async users (obj, args, context, info) {
}, // -> Sanitize limit
UserQuery: { let limit = args.pageSize ?? 20
async list(obj, args, context, info) { if (limit < 1 || limit > 1000) {
return WIKI.models.users.query() limit = 1000
.select('id', 'email', 'name', 'providerKey', 'isSystem', 'isActive', 'createdAt', 'lastLoginAt') }
},
async search(obj, args, context, info) { // -> Sanitize offset
let offset = args.page ?? 1
if (offset < 1) {
offset = 1
}
// -> Fetch Users
return WIKI.models.users.query() return WIKI.models.users.query()
.where('email', 'like', `%${args.query}%`) .select('id', 'email', 'name', 'isSystem', 'isActive', 'createdAt', 'lastLoginAt')
.orWhere('name', 'like', `%${args.query}%`) .where(builder => {
.limit(10) if (args.filter) {
.select('id', 'email', 'name', 'providerKey', 'createdAt') 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) * FETCH A SINGLE USER
usr.password = '' */
usr.tfaSecret = '' async userById (obj, args, context, info) {
const usr = await WIKI.models.users.query().findById(args.id)
const str = _.get(WIKI.auth.strategies, usr.providerKey) // const str = _.get(WIKI.auth.strategies, usr.providerKey)
str.strategy = _.find(WIKI.data.authentication, ['key', str.strategyKey]) // str.strategy = _.find(WIKI.data.authentication, ['key', str.strategyKey])
usr.providerName = str.displayName // usr.providerName = str.displayName
usr.providerIs2FACapable = _.get(str, 'strategy.useForm', false) // 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 return usr
}, },
@ -61,19 +83,19 @@ module.exports = {
.limit(10) .limit(10)
} }
}, },
UserMutation: { Mutation: {
async create (obj, args) { async createUser (obj, args) {
try { try {
await WIKI.models.users.createNewUser(args) await WIKI.models.users.createNewUser({ ...args, passwordRaw: args.password, isVerified: true })
return { return {
responseResult: graphHelper.generateSuccess('User created successfully') status: graphHelper.generateSuccess('User created successfully')
} }
} catch (err) { } catch (err) {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
async delete (obj, args) { async deleteUser (obj, args) {
try { try {
if (args.id <= 2) { if (args.id <= 2) {
throw new WIKI.Error.UserDeleteProtected() throw new WIKI.Error.UserDeleteProtected()
@ -84,7 +106,7 @@ module.exports = {
WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' }) WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })
return { return {
responseResult: graphHelper.generateSuccess('User deleted successfully') status: graphHelper.generateSuccess('User deleted successfully')
} }
} catch (err) { } catch (err) {
if (err.message.indexOf('foreign') >= 0) { if (err.message.indexOf('foreign') >= 0) {
@ -94,40 +116,40 @@ module.exports = {
} }
} }
}, },
async update (obj, args) { async updateUser (obj, args) {
try { try {
await WIKI.models.users.updateUser(args) await WIKI.models.users.updateUser(args.id, args.patch)
return { return {
responseResult: graphHelper.generateSuccess('User created successfully') status: graphHelper.generateSuccess('User updated successfully')
} }
} catch (err) { } catch (err) {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
async verify (obj, args) { async verifyUser (obj, args) {
try { try {
await WIKI.models.users.query().patch({ isVerified: true }).findById(args.id) await WIKI.models.users.query().patch({ isVerified: true }).findById(args.id)
return { return {
responseResult: graphHelper.generateSuccess('User verified successfully') status: graphHelper.generateSuccess('User verified successfully')
} }
} catch (err) { } catch (err) {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
async activate (obj, args) { async activateUser (obj, args) {
try { try {
await WIKI.models.users.query().patch({ isActive: true }).findById(args.id) await WIKI.models.users.query().patch({ isActive: true }).findById(args.id)
return { return {
responseResult: graphHelper.generateSuccess('User activated successfully') status: graphHelper.generateSuccess('User activated successfully')
} }
} catch (err) { } catch (err) {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
async deactivate (obj, args) { async deactivateUser (obj, args) {
try { try {
if (args.id <= 2) { if (args.id <= 2) {
throw new Error('Cannot deactivate system accounts.') throw new Error('Cannot deactivate system accounts.')
@ -138,35 +160,35 @@ module.exports = {
WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' }) WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })
return { return {
responseResult: graphHelper.generateSuccess('User deactivated successfully') status: graphHelper.generateSuccess('User deactivated successfully')
} }
} catch (err) { } catch (err) {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
async enableTFA (obj, args) { async enableUserTFA (obj, args) {
try { try {
await WIKI.models.users.query().patch({ tfaIsActive: true, tfaSecret: null }).findById(args.id) await WIKI.models.users.query().patch({ tfaIsActive: true, tfaSecret: null }).findById(args.id)
return { return {
responseResult: graphHelper.generateSuccess('User 2FA enabled successfully') status: graphHelper.generateSuccess('User 2FA enabled successfully')
} }
} catch (err) { } catch (err) {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
async disableTFA (obj, args) { async disableUserTFA (obj, args) {
try { try {
await WIKI.models.users.query().patch({ tfaIsActive: false, tfaSecret: null }).findById(args.id) await WIKI.models.users.query().patch({ tfaIsActive: false, tfaSecret: null }).findById(args.id)
return { return {
responseResult: graphHelper.generateSuccess('User 2FA disabled successfully') status: graphHelper.generateSuccess('User 2FA disabled successfully')
} }
} catch (err) { } catch (err) {
return graphHelper.generateError(err) return graphHelper.generateError(err)
} }
}, },
resetPassword (obj, args) { resetUserPassword (obj, args) {
return false return false
}, },
async updateProfile (obj, args, context) { async updateProfile (obj, args, context) {
@ -203,7 +225,7 @@ module.exports = {
const newToken = await WIKI.models.users.refreshToken(usr.id) const newToken = await WIKI.models.users.refreshToken(usr.id)
return { return {
responseResult: graphHelper.generateSuccess('User profile updated successfully'), status: graphHelper.generateSuccess('User profile updated successfully'),
jwt: newToken.token jwt: newToken.token
} }
} catch (err) { } catch (err) {

@ -1,8 +1,6 @@
const gql = require('graphql') const gql = require('graphql')
module.exports = { module.exports = new gql.GraphQLScalarType({
Date: new gql.GraphQLScalarType({
name: 'Date', name: 'Date',
description: 'ISO date-time string at UTC', description: 'ISO date-time string at UTC',
parseValue(value) { parseValue(value) {
@ -17,5 +15,4 @@ module.exports = {
} }
return new Date(ast.value) return new Date(ast.value)
} }
}) })
}

@ -39,8 +39,7 @@ function parseObject (typeName, ast, variables) {
return value return value
} }
module.exports = { module.exports = new GraphQLScalarType({
JSON: new GraphQLScalarType({
name: 'JSON', name: 'JSON',
description: 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).', 'The `JSON` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).',
@ -55,5 +54,4 @@ module.exports = {
return parseObject('JSONObject', ast, variables) return parseObject('JSONObject', ast, variables)
} }
}) })
}

@ -1,5 +1,4 @@
const { Kind, GraphQLScalarType } = require('graphql') 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 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' const nilUUID = '00000000-0000-0000-0000-000000000000'
@ -8,10 +7,9 @@ function isUUID (value) {
return uuidRegex.test(value) || nilUUID === value return uuidRegex.test(value) || nilUUID === value
} }
module.exports = { module.exports = new GraphQLScalarType({
UUID: new GraphQLScalarType({
name: 'UUID', name: 'UUID',
description: 'The `UUID` scalar type represents UUID values as specified by [RFC 4122](https://tools.ietf.org/html/rfc4122).', description: 'The `UUID` scalar type represents UUID values as specified by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122).',
serialize: (value) => { serialize: (value) => {
if (!isUUID(value)) { if (!isUUID(value)) {
throw new TypeError(`UUID cannot represent non-UUID value: ${value}`) throw new TypeError(`UUID cannot represent non-UUID value: ${value}`)
@ -35,5 +33,4 @@ module.exports = {
return undefined return undefined
} }
}) })
}

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

@ -3,49 +3,33 @@
# =============================================== # ===============================================
extend type Query { extend type Query {
assets: AssetQuery assets(
}
extend type Mutation {
assets: AssetMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type AssetQuery {
list(
folderId: Int! folderId: Int!
kind: AssetKind! kind: AssetKind!
): [AssetItem] @auth(requires: ["manage:system", "read:assets"]) ): [AssetItem]
folders( assetsFolders(
parentFolderId: Int! parentFolderId: Int!
): [AssetFolder] @auth(requires: ["manage:system", "read:assets"]) ): [AssetFolder]
} }
# ----------------------------------------------- extend type Mutation {
# MUTATIONS createAssetsFolder(
# -----------------------------------------------
type AssetMutation {
createFolder(
parentFolderId: Int! parentFolderId: Int!
slug: String! slug: String!
name: String name: String
): DefaultResponse @auth(requires: ["manage:system", "write:assets"]) ): DefaultResponse
renameAsset( renameAsset(
id: Int! id: Int!
filename: String! filename: String!
): DefaultResponse @auth(requires: ["manage:system", "manage:assets"]) ): DefaultResponse
deleteAsset( deleteAsset(
id: Int! 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 { extend type Query {
authentication: AuthenticationQuery apiKeys: [AuthenticationApiKey]
}
extend type Mutation {
authentication: AuthenticationMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type AuthenticationQuery { apiState: Boolean
apiKeys: [AuthenticationApiKey] @auth(requires: ["manage:system", "manage:api"])
apiState: Boolean! @auth(requires: ["manage:system", "manage:api"]) authStrategies(
strategies: [AuthenticationStrategy] @auth(requires: ["manage:system"])
activeStrategies(
enabledOnly: Boolean enabledOnly: Boolean
): [AuthenticationActiveStrategy] ): [AuthenticationStrategy]
} }
# ----------------------------------------------- extend type Mutation {
# MUTATIONS
# -----------------------------------------------
type AuthenticationMutation {
createApiKey( createApiKey(
name: String! name: String!
expiration: String! expiration: String!
fullAccess: Boolean! fullAccess: Boolean!
group: Int group: Int
): AuthenticationCreateApiKeyResponse @auth(requires: ["manage:system", "manage:api"]) ): AuthenticationCreateApiKeyResponse
login( login(
username: String! username: String!
@ -67,19 +49,19 @@ type AuthenticationMutation {
revokeApiKey( revokeApiKey(
id: Int! id: Int!
): DefaultResponse @auth(requires: ["manage:system", "manage:api"]) ): DefaultResponse
setApiState( setApiState(
enabled: Boolean! enabled: Boolean!
): DefaultResponse @auth(requires: ["manage:system", "manage:api"]) ): DefaultResponse
updateStrategies( updateAuthStrategies(
strategies: [AuthenticationStrategyInput]! 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 { type AuthenticationLoginResponse {
responseResult: ResponseStatus operation: Operation
jwt: String jwt: String
mustChangePwd: Boolean mustChangePwd: Boolean
mustProvideTFA: Boolean mustProvideTFA: Boolean
@ -124,7 +106,7 @@ type AuthenticationLoginResponse {
} }
type AuthenticationRegisterResponse { type AuthenticationRegisterResponse {
responseResult: ResponseStatus operation: Operation
jwt: String jwt: String
} }
@ -151,6 +133,6 @@ type AuthenticationApiKey {
} }
type AuthenticationCreateApiKeyResponse { type AuthenticationCreateApiKeyResponse {
responseResult: ResponseStatus operation: Operation
key: String key: String
} }

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

@ -2,7 +2,7 @@
# Wiki.js GraphQL Schema # # Wiki.js GraphQL Schema #
# ====================== # # ====================== #
# DIRECTIVES # DIRECTIVES (deprecated)
# ---------- # ----------
directive @auth(requires: [String]) on QUERY | FIELD_DEFINITION | ARGUMENT_DEFINITION 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 # Generic Key Value Pair
type KeyValuePair { type KeyValuePair {
key: String! key: String
value: String! value: String
} }
# General Key Value Pair Input # General Key Value Pair Input
input KeyValuePairInput { input KeyValuePairInput {
@ -23,14 +23,13 @@ input KeyValuePairInput {
# Generic Mutation Response # Generic Mutation Response
type DefaultResponse { type DefaultResponse {
responseResult: ResponseStatus operation: Operation
} }
# Mutation Status # Mutation Operation
type ResponseStatus { type Operation {
succeeded: Boolean! succeeded: Boolean
errorCode: Int! slug: String
slug: String!
message: String message: String
} }
@ -47,6 +46,3 @@ type Query
# Mutations (Create, Update, Delete) # Mutations (Create, Update, Delete)
type Mutation 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 { extend type Query {
groups: GroupQuery groups(
}
extend type Mutation {
groups: GroupMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type GroupQuery {
list(
filter: String filter: String
orderBy: String orderBy: String
): [GroupMinimal] @auth(requires: ["write:groups", "manage:groups", "manage:system"]) ): [Group]
single( groupById(
id: Int! id: Int!
): Group @auth(requires: ["write:groups", "manage:groups", "manage:system"]) ): Group
} }
# ----------------------------------------------- extend type Mutation {
# MUTATIONS createGroup(
# -----------------------------------------------
type GroupMutation {
create(
name: String! name: String!
): GroupResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"]) ): GroupResponse
update( updateGroup(
id: Int! id: Int!
name: String! patch: GroupUpdateInput!
redirectOnLogin: String! ): DefaultResponse
permissions: [String]!
pageRules: [PageRuleInput]!
): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"])
delete( deleteGroup(
id: Int! id: Int!
): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"]) ): DefaultResponse
assignUser( assignUserToGroup(
groupId: Int! groupId: Int!
userId: Int! userId: Int!
): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"]) ): DefaultResponse
unassignUser( unassignUserFromGroup(
groupId: Int! groupId: Int!
userId: Int! userId: Int!
): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"]) ): DefaultResponse
} }
# ----------------------------------------------- # -----------------------------------------------
@ -62,38 +43,36 @@ type GroupMutation {
# ----------------------------------------------- # -----------------------------------------------
type GroupResponse { type GroupResponse {
responseResult: ResponseStatus! operation: Operation
group: Group group: Group
} }
type GroupMinimal {
id: Int!
name: String!
isSystem: Boolean!
userCount: Int
createdAt: Date!
updatedAt: Date!
}
type Group { type Group {
id: Int! id: Int
name: String! name: String
isSystem: Boolean! isSystem: Boolean
redirectOnLogin: String redirectOnLogin: String
permissions: [String]! permissions: [String]
pageRules: [PageRule] pageRules: [PageRule]
users: [UserMinimal] users: [UserMinimal]
createdAt: Date! createdAt: Date
updatedAt: Date! updatedAt: Date
} }
type PageRule { type PageRule {
id: String! id: String
deny: Boolean! deny: Boolean
match: PageRuleMatch! match: PageRuleMatch
roles: [String]! roles: [String]
path: String! path: String
locales: [String]! locales: [String]
}
input GroupUpdateInput {
name: String!
redirectOnLogin: String!
permissions: [String]!
pageRules: [PageRuleInput]!
} }
input PageRuleInput { input PageRuleInput {

@ -3,38 +3,21 @@
# =============================================== # ===============================================
extend type Query { extend type Query {
localization: LocalizationQuery
}
extend type Mutation {
localization: LocalizationMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type LocalizationQuery {
locales: [LocalizationLocale] locales: [LocalizationLocale]
config: LocalizationConfig
translations(locale: String!, namespace: String!): [Translation] translations(locale: String!, namespace: String!): [Translation]
} }
# ----------------------------------------------- extend type Mutation {
# MUTATIONS
# -----------------------------------------------
type LocalizationMutation {
downloadLocale( downloadLocale(
locale: String! locale: String!
): DefaultResponse @auth(requires: ["manage:system"]) ): DefaultResponse
updateLocale( updateLocale(
locale: String! locale: String!
autoUpdate: Boolean! autoUpdate: Boolean!
namespacing: Boolean! namespacing: Boolean!
namespaces: [String]! namespaces: [String]!
): DefaultResponse @auth(requires: ["manage:system"]) ): DefaultResponse
} }
# ----------------------------------------------- # -----------------------------------------------
@ -42,25 +25,18 @@ type LocalizationMutation {
# ----------------------------------------------- # -----------------------------------------------
type LocalizationLocale { type LocalizationLocale {
availability: Int! availability: Int
code: String! code: String
createdAt: Date! createdAt: Date
installDate: Date installDate: Date
isInstalled: Boolean! isInstalled: Boolean
isRTL: Boolean! isRTL: Boolean
name: String! name: String
nativeName: String! nativeName: String
updatedAt: Date! updatedAt: Date
}
type LocalizationConfig {
locale: String!
autoUpdate: Boolean!
namespacing: Boolean!
namespaces: [String]!
} }
type Translation { type Translation {
key: String! key: String
value: 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 { extend type Query {
mail: MailQuery mailConfig: MailConfig
} }
extend type Mutation { extend type Mutation {
mail: MailMutation sendMailTest(
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type MailQuery {
config: MailConfig @auth(requires: ["manage:system"])
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type MailMutation {
sendTest(
recipientEmail: String! recipientEmail: String!
): DefaultResponse @auth(requires: ["manage:system"]) ): DefaultResponse
updateConfig( updateMailConfig(
senderName: String! senderName: String!
senderEmail: String! senderEmail: String!
host: String! host: String!
@ -40,7 +24,7 @@ type MailMutation {
dkimDomainName: String! dkimDomainName: String!
dkimKeySelector: String! dkimKeySelector: String!
dkimPrivateKey: String! dkimPrivateKey: String!
): DefaultResponse @auth(requires: ["manage:system"]) ): DefaultResponse
} }
# ----------------------------------------------- # -----------------------------------------------
@ -48,16 +32,16 @@ type MailMutation {
# ----------------------------------------------- # -----------------------------------------------
type MailConfig { type MailConfig {
senderName: String! senderName: String
senderEmail: String! senderEmail: String
host: String! host: String
port: Int! port: Int
secure: Boolean! secure: Boolean
verifySSL: Boolean! verifySSL: Boolean
user: String! user: String
pass: String! pass: String
useDKIM: Boolean! useDKIM: Boolean
dkimDomainName: String! dkimDomainName: String
dkimKeySelector: String! dkimKeySelector: String
dkimPrivateKey: String! dkimPrivateKey: String
} }

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

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

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

@ -2,54 +2,10 @@
# SEARCH # SEARCH
# =============================================== # ===============================================
extend type Query {
search: SearchQuery
}
extend type Mutation { extend type Mutation {
search: SearchMutation rebuildSearchIndex: DefaultResponse
}
# -----------------------------------------------
# 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"])
} }
# ----------------------------------------------- # -----------------------------------------------
# TYPES # 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! hostname: String!
exact: Boolean! exact: Boolean!
): Site @auth(requires: ["manage:system"]) ): Site @auth(requires: ["manage:system"])
# Legacy
site: SiteQuery
} }
extend type Mutation { extend type Mutation {
@ -42,59 +39,6 @@ extend type Mutation {
deleteSite ( deleteSite (
id: UUID! id: UUID!
): DefaultResponse @auth(requires: ["manage:system"]) ): 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 { type SiteCreateResponse {
status: ResponseStatus operation: Operation
site: Site site: Site
} }
@ -227,40 +171,3 @@ input SiteThemeInput {
showSharingMenu: Boolean showSharingMenu: Boolean
showPrintBtn: 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 { extend type Query {
storage: StorageQuery storageTargets(
siteId: UUID!
): [StorageTarget]
} }
extend type Mutation { extend type Mutation {
storage: StorageMutation updateStorageTargets(
} siteId: UUID!
targets: [StorageTargetInput]!
# ----------------------------------------------- ): DefaultResponse
# QUERIES
# -----------------------------------------------
type StorageQuery {
targets: [StorageTarget] @auth(requires: ["manage:system"])
status: [StorageStatus] @auth(requires: ["manage:system"])
}
# ----------------------------------------------- setupStorageTarget(
# MUTATIONS targetId: UUID!
# ----------------------------------------------- state: JSON!
): StorageTargetSetupResponse
type StorageMutation { destroyStorageTargetSetup(
updateTargets( targetId: UUID!
targets: [StorageTargetInput]! ): DefaultResponse
): DefaultResponse @auth(requires: ["manage:system"])
executeAction( executeStorageAction(
targetKey: String! targetId: UUID!
handler: String! handler: String!
): DefaultResponse @auth(requires: ["manage:system"]) ): DefaultResponse
} }
# ----------------------------------------------- # -----------------------------------------------
@ -39,40 +34,46 @@ type StorageMutation {
# ----------------------------------------------- # -----------------------------------------------
type StorageTarget { type StorageTarget {
isAvailable: Boolean! id: UUID
isEnabled: Boolean! isEnabled: Boolean
key: String! module: String
title: String! title: String
description: String description: String
logo: String icon: String
banner: String
vendor: String
website: String website: String
supportedModes: [String] contentTypes: JSON
mode: String assetDelivery: JSON
hasSchedule: Boolean! versioning: JSON
syncInterval: String sync: JSON
syncIntervalDefault: String status: JSON
config: [KeyValuePair] setup: JSON
actions: [StorageTargetAction] config: JSON
actions: JSON
} }
input StorageTargetInput { type StorageTargetSetupResponse {
isEnabled: Boolean! operation: Operation
key: String! state: JSON
mode: String!
syncInterval: String
config: [KeyValuePairInput]
} }
type StorageStatus { input StorageTargetInput {
key: String! id: UUID!
title: String! module: String!
status: String! isEnabled: Boolean
message: String! contentTypes: [String!]
lastAttempt: String! largeThreshold: String
assetDeliveryFileStreaming: Boolean
assetDeliveryDirectAccess: Boolean
syncMode: StorageTargetSyncMode
syncInterval: String
useVersioning: Boolean
config: JSON
} }
type StorageTargetAction { enum StorageTargetSyncMode {
handler: String! PULL
label: String! PUSH
hint: String! SYNC
} }

@ -3,50 +3,41 @@
# =============================================== # ===============================================
extend type Query { extend type Query {
system: SystemQuery systemExtensions: [SystemExtension]
systemFlags: [SystemFlag]
systemInfo: SystemInfo
systemSecurity: SystemSecurity
} }
extend type Mutation { extend type Mutation {
system: SystemMutation updateSystemFlags(
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type SystemQuery {
flags: [SystemFlag] @auth(requires: ["manage:system"])
info: SystemInfo
extensions: [SystemExtension]! @auth(requires: ["manage:system"])
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type SystemMutation {
updateFlags(
flags: [SystemFlagInput]! flags: [SystemFlagInput]!
): DefaultResponse @auth(requires: ["manage:system"]) ): DefaultResponse
resetTelemetryClientId: DefaultResponse @auth(requires: ["manage:system"]) updateSystemSecurity(
authJwtAudience: String
setTelemetry( authJwtExpiration: String
enabled: Boolean! authJwtRenewablePeriod: String
): DefaultResponse @auth(requires: ["manage:system"]) corsConfig: String
corsMode: SystemSecurityCorsMode
performUpgrade: DefaultResponse @auth(requires: ["manage:system"]) cspDirectives: String
disallowFloc: Boolean
importUsersFromV1( disallowIframe: Boolean
mongoDbConnString: String! disallowOpenRedirect: Boolean
groupMode: SystemImportUsersGroupMode! enforceCsp: Boolean
): SystemImportUsersResponse @auth(requires: ["manage:system"]) enforceHsts: Boolean
enforceSameOriginReferrerPolicy: Boolean
setHTTPSRedirection( forceAssetDownload: Boolean
enabled: Boolean! hstsDuration: Int
): DefaultResponse @auth(requires: ["manage:system"]) trustProxy: Boolean
uploadMaxFiles: Int
renewHTTPSCertificate: DefaultResponse @auth(requires: ["manage:system"]) uploadMaxFileSize: Int
uploadScanSVG: Boolean
): DefaultResponse
installExtension(
key: String!
): DefaultResponse
} }
# ----------------------------------------------- # -----------------------------------------------
@ -54,8 +45,8 @@ type SystemMutation {
# ----------------------------------------------- # -----------------------------------------------
type SystemFlag { type SystemFlag {
key: String! key: String
value: Boolean! value: Boolean
} }
input SystemFlagInput { input SystemFlagInput {
@ -64,35 +55,35 @@ input SystemFlagInput {
} }
type SystemInfo { type SystemInfo {
configFile: String @auth(requires: ["manage:system"]) configFile: String
cpuCores: Int @auth(requires: ["manage:system"]) cpuCores: Int
currentVersion: String @auth(requires: ["manage:system"]) currentVersion: String
dbHost: String @auth(requires: ["manage:system"]) dbHost: String
dbType: String @auth(requires: ["manage:system"]) dbType: String
dbVersion: String @auth(requires: ["manage:system"]) dbVersion: String
groupsTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:groups", "write:groups", "manage:users", "write:users"]) groupsTotal: Int
hostname: String @auth(requires: ["manage:system"]) hostname: String
httpPort: Int @auth(requires: ["manage:system"]) httpPort: Int
httpRedirection: Boolean @auth(requires: ["manage:system"]) httpRedirection: Boolean
httpsPort: Int @auth(requires: ["manage:system"]) httpsPort: Int
latestVersion: String @auth(requires: ["manage:system"]) latestVersion: String
latestVersionReleaseDate: Date @auth(requires: ["manage:system"]) latestVersionReleaseDate: Date
nodeVersion: String @auth(requires: ["manage:system"]) nodeVersion: String
operatingSystem: String @auth(requires: ["manage:system"]) operatingSystem: String
pagesTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:pages", "delete:pages"]) pagesTotal: Int
platform: String @auth(requires: ["manage:system"]) platform: String
ramTotal: String @auth(requires: ["manage:system"]) ramTotal: String
sslDomain: String @auth(requires: ["manage:system"]) sslDomain: String
sslExpirationDate: Date @auth(requires: ["manage:system"]) sslExpirationDate: Date
sslProvider: String @auth(requires: ["manage:system"]) sslProvider: String
sslStatus: String @auth(requires: ["manage:system"]) sslStatus: String
sslSubscriberEmail: String @auth(requires: ["manage:system"]) sslSubscriberEmail: String
tagsTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:pages", "delete:pages"]) tagsTotal: Int
telemetry: Boolean @auth(requires: ["manage:system"]) telemetry: Boolean
telemetryClientId: String @auth(requires: ["manage:system"]) telemetryClientId: String
upgradeCapable: Boolean @auth(requires: ["manage:system"]) upgradeCapable: Boolean
usersTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:groups", "write:groups", "manage:users", "write:users"]) usersTotal: Int
workingDirectory: String @auth(requires: ["manage:system"]) workingDirectory: String
} }
enum SystemImportUsersGroupMode { enum SystemImportUsersGroupMode {
@ -102,7 +93,7 @@ enum SystemImportUsersGroupMode {
} }
type SystemImportUsersResponse { type SystemImportUsersResponse {
responseResult: ResponseStatus operation: Operation
usersCount: Int usersCount: Int
groupsCount: Int groupsCount: Int
failed: [SystemImportUsersResponseFailed] failed: [SystemImportUsersResponseFailed]
@ -115,9 +106,38 @@ type SystemImportUsersResponseFailed {
} }
type SystemExtension { type SystemExtension {
key: String! key: String
title: String! title: String
description: String! description: String
isInstalled: Boolean! isInstalled: Boolean
isCompatible: 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 { extend type Query {
users: UserQuery users (
} page: Int
pageSize: Int
extend type Mutation { orderBy: UserOrderBy
users: UserMutation orderByDirection: OrderByDirection
} # Filter by name / email
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type UserQuery {
list(
filter: String filter: String
orderBy: String ): [UserMinimal]
): [UserMinimal] @auth(requires: ["write:users", "manage:users", "manage:system"])
search( userById(
query: String! id: UUID!
): [UserMinimal] @auth(requires: ["write:groups", "manage:groups", "write:users", "manage:users", "manage:system"]) ): User
single(
id: Int!
): User @auth(requires: ["manage:users", "manage:system"])
profile: UserProfile profile: UserProfile
lastLogins: [UserLastLogin] @auth(requires: ["write:groups", "manage:groups", "write:users", "manage:users", "manage:system"]) lastLogins: [UserLastLogin]
} }
# ----------------------------------------------- extend type Mutation {
# MUTATIONS createUser(
# -----------------------------------------------
type UserMutation {
create(
email: String! email: String!
name: String! name: String!
passwordRaw: String password: String!
providerKey: String! groups: [UUID]!
groups: [Int]! mustChangePassword: Boolean!
mustChangePassword: Boolean sendWelcomeEmail: Boolean!
sendWelcomeEmail: Boolean ): UserResponse
): UserResponse @auth(requires: ["write:users", "manage:users", "manage:system"])
updateUser(
update( id: UUID!
id: Int! patch: UserUpdateInput!
email: String ): DefaultResponse
name: String
newPassword: String
groups: [Int]
location: String
jobTitle: String
timezone: String
dateFormat: String
appearance: String
): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
delete( deleteUser(
id: Int! id: UUID!
replaceId: Int! replaceId: UUID!
): DefaultResponse @auth(requires: ["manage:users", "manage:system"]) ): DefaultResponse
verify( verifyUser(
id: Int! id: UUID!
): DefaultResponse @auth(requires: ["manage:users", "manage:system"]) ): DefaultResponse
activate( activateUser(
id: Int! id: UUID!
): DefaultResponse @auth(requires: ["manage:users", "manage:system"]) ): DefaultResponse
deactivate( deactivateUser(
id: Int! id: UUID!
): DefaultResponse @auth(requires: ["manage:users", "manage:system"]) ): DefaultResponse
enableTFA( enableUserTFA(
id: Int! id: UUID!
): DefaultResponse @auth(requires: ["manage:users", "manage:system"]) ): DefaultResponse
disableTFA( disableUserTFA(
id: Int! id: UUID!
): DefaultResponse @auth(requires: ["manage:users", "manage:system"]) ): DefaultResponse
resetPassword( resetUserPassword(
id: Int! id: Int!
): DefaultResponse ): DefaultResponse
@ -110,71 +85,83 @@ type UserMutation {
# ----------------------------------------------- # -----------------------------------------------
type UserResponse { type UserResponse {
responseResult: ResponseStatus! operation: Operation
user: User user: User
} }
type UserLastLogin { type UserLastLogin {
id: Int! id: UUID
name: String! name: String
lastLoginAt: Date! lastLoginAt: Date
} }
type UserMinimal { type UserMinimal {
id: Int! id: UUID
name: String! name: String
email: String! email: String
providerKey: String! isSystem: Boolean
isSystem: Boolean! isActive: Boolean
isActive: Boolean! createdAt: Date
createdAt: Date!
lastLoginAt: Date lastLoginAt: Date
} }
type User { type User {
id: Int! id: UUID
name: String! name: String
email: String! email: String
providerKey: String! auth: JSON
providerName: String isSystem: Boolean
providerId: String isActive: Boolean
providerIs2FACapable: Boolean isVerified: Boolean
isSystem: Boolean! meta: JSON
isActive: Boolean! prefs: JSON
isVerified: Boolean! createdAt: Date
location: String! updatedAt: Date
jobTitle: String!
timezone: String!
dateFormat: String!
appearance: String!
createdAt: Date!
updatedAt: Date!
lastLoginAt: Date lastLoginAt: Date
tfaIsActive: Boolean! groups: [Group]
groups: [Group]!
} }
type UserProfile { type UserProfile {
id: Int! id: Int
name: String! name: String
email: String! email: String
providerKey: String providerKey: String
providerName: String providerName: String
isSystem: Boolean! isSystem: Boolean
isVerified: Boolean! isVerified: Boolean
location: String! location: String
jobTitle: String! jobTitle: String
timezone: String! timezone: String
dateFormat: String! dateFormat: String
appearance: String! appearance: String
createdAt: Date! createdAt: Date
updatedAt: Date! updatedAt: Date
lastLoginAt: Date lastLoginAt: Date
groups: [String]! groups: [String]
pagesTotal: Int! pagesTotal: Int
} }
type UserTokenResponse { type UserTokenResponse {
responseResult: ResponseStatus! operation: Operation
jwt: String 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,65 +1,86 @@
<template lang="pug"> <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(style='min-width: 350px; max-width: 450px;')
q-card-section.card-header q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-shutdown.svg', left, size='sm') 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 q-card-section
.text-body2 .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) template(v-slot:siteTitle)
strong {{site.title}} strong {{props.site.title}}
q-card-actions.card-actions q-card-actions.card-actions
q-space q-space
q-btn.acrylic-btn( q-btn.acrylic-btn(
flat flat
:label='$t(`common.actions.cancel`)' :label='t(`common.actions.cancel`)'
color='grey' color='grey'
padding='xs md' padding='xs md'
@click='hide' @click='onDialogCancel'
) )
q-btn( q-btn(
unelevated unelevated
:label='value ? $t(`common.actions.activate`) : $t(`common.actions.deactivate`)' :label='modelValue ? t(`common.actions.activate`) : t(`common.actions.deactivate`)'
:color='value ? `positive` : `negative`' :color='modelValue ? `positive` : `negative`'
padding='xs md' padding='xs md'
@click='confirm' @click='confirm'
:loading='state.isLoading'
) )
</template> </template>
<script> <script setup>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import cloneDeep from 'lodash/cloneDeep' import cloneDeep from 'lodash/cloneDeep'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
import { reactive, ref } from 'vue'
export default { import { useAdminStore } from '../stores/admin'
props: {
// PROPS
const props = defineProps({
site: { site: {
type: Object type: Object,
required: true
}, },
value: { modelValue: {
type: Boolean, type: Boolean,
default: false default: false
} }
}, })
emits: ['ok', 'hide'],
data () { // EMITS
return {
} defineEmits([
}, ...useDialogPluginComponent.emits
methods: { ])
show () {
this.$refs.dialog.show() // QUASAR
},
hide () { const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
this.$refs.dialog.hide() const $q = useQuasar()
},
onDialogHide () { // STORES
this.$emit('hide')
}, const adminStore = useAdminStore()
async confirm () {
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
isLoading: false
})
// METHODS
async function confirm () {
state.isLoading = true
try { try {
const siteId = this.site.id const resp = await APOLLO_CLIENT.mutate({
const resp = await this.$apollo.mutate({
mutation: gql` mutation: gql`
mutation updateSite ( mutation updateSite (
$id: UUID! $id: UUID!
@ -71,7 +92,7 @@ export default {
isEnabled: $newState isEnabled: $newState
} }
) { ) {
status { operation {
succeeded succeeded
message message
} }
@ -79,36 +100,36 @@ export default {
} }
`, `,
variables: { variables: {
id: siteId, id: props.site.id,
newState: this.value newState: props.modelValue
} }
}) })
if (resp?.data?.updateSite?.status?.succeeded) { if (resp?.data?.updateSite?.operation?.succeeded) {
this.$q.notify({ $q.notify({
type: 'positive', type: 'positive',
message: this.$t('admin.sites.updateSuccess') message: this.$t('admin.sites.updateSuccess')
}) })
this.$store.set('admin/sites', this.$store.get('admin/sites').map(s => { adminStore.$patch({
if (s.id === siteId) { sites: adminStore.sites.map(s => {
if (s.id === props.site.id) {
const ns = cloneDeep(s) const ns = cloneDeep(s)
ns.isEnabled = this.value ns.isEnabled = props.modelValue
return ns return ns
} else { } else {
return s return s
} }
})) })
this.$emit('ok') })
this.hide() onDialogOK()
} else { } else {
throw new Error(resp?.data?.updateSite?.status?.message || 'An unexpected error occured.') throw new Error(resp?.data?.updateSite?.operation?.message || 'An unexpected error occured.')
} }
} catch (err) { } catch (err) {
this.$q.notify({ $q.notify({
type: 'negative', type: 'negative',
message: err.message message: err.message
}) })
} }
} state.isLoading = false
}
} }
</script> </script>

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

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

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

Loading…
Cancel
Save