Unverified Commit 358ad1fd authored by NGPixel's avatar NGPixel

refactor: update schema with new structure / naming

parent dfde2e10
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
const { createRateLimitDirective } = require('graphql-rate-limit-directive')
module.exports = createRateLimitDirective({
keyGenerator: (directiveArgs, source, args, context, info) => `${context.req.ip}:${info.parentType}.${info.fieldName}`
})
......@@ -3,16 +3,20 @@ const fs = require('fs')
const path = require('path')
const autoload = require('auto-load')
const { makeExecutableSchema } = require('@graphql-tools/schema')
const { rateLimitDirective } = require('graphql-rate-limit-directive')
const { defaultKeyGenerator, rateLimitDirective } = require('graphql-rate-limit-directive')
const { GraphQLUpload } = require('graphql-upload')
const { rateLimitDirectiveTypeDefs, rateLimitDirectiveTransformer } = rateLimitDirective()
/* global WIKI */
WIKI.logger.info(`Loading GraphQL Schema...`)
// Rate Limiter
const { rateLimitDirectiveTypeDefs, rateLimitDirectiveTransformer } = rateLimitDirective({
keyGenerator: (directiveArgs, source, args, context, info) => `${context.req.ip}:${defaultKeyGenerator(directiveArgs, source, args, context, info)}`
})
// Schemas
WIKI.logger.info(`Loading GraphQL Schema...`)
const typeDefs = [
rateLimitDirectiveTypeDefs
]
......@@ -23,7 +27,11 @@ schemas.forEach(schema => {
// Resolvers
WIKI.logger.info(`Loading GraphQL Resolvers...`)
let resolvers = {
Date: require('./scalars/date'),
JSON: require('./scalars/json'),
UUID: require('./scalars/uuid'),
Upload: GraphQLUpload
}
const resolversObj = _.values(autoload(path.join(WIKI.SERVERPATH, 'graph/resolvers')))
......@@ -33,11 +41,14 @@ resolversObj.forEach(resolver => {
// Make executable schema
WIKI.logger.info(`Compiling GraphQL Schema...`)
let schema = makeExecutableSchema({
typeDefs,
resolvers
})
// Apply schema transforms
schema = rateLimitDirectiveTransformer(schema)
WIKI.logger.info(`GraphQL Schema: [ OK ]`)
......
......@@ -5,13 +5,7 @@ const graphHelper = require('../../helpers/graph')
module.exports = {
Query: {
async analytics() { return {} }
},
Mutation: {
async analytics() { return {} }
},
AnalyticsQuery: {
async providers(obj, args, context, info) {
async analyticsProviders(obj, args, context, info) {
let providers = await WIKI.models.analytics.getProviders(args.isEnabled)
providers = providers.map(stg => {
const providerInfo = _.find(WIKI.data.analytics, ['key', stg.key]) || {}
......@@ -33,8 +27,8 @@ module.exports = {
return providers
}
},
AnalyticsMutation: {
async updateProviders(obj, args, context) {
Mutation: {
async updateAnalyticsProviders(obj, args, context) {
try {
for (let str of args.providers) {
await WIKI.models.analytics.query().patch({
......
......@@ -7,13 +7,7 @@ const assetHelper = require('../../helpers/asset')
module.exports = {
Query: {
async assets() { return {} }
},
Mutation: {
async assets() { return {} }
},
AssetQuery: {
async list(obj, args, context) {
async assets(obj, args, context) {
let cond = {
folderId: args.folderId === 0 ? null : args.folderId
}
......@@ -31,7 +25,7 @@ module.exports = {
kind: a.kind.toUpperCase()
}))
},
async folders(obj, args, context) {
async assetsFolders(obj, args, context) {
const results = await WIKI.models.assetFolders.query().where({
parentId: args.parentFolderId === 0 ? null : args.parentFolderId
})
......@@ -43,11 +37,11 @@ module.exports = {
})
}
},
AssetMutation: {
Mutation: {
/**
* Create New Asset Folder
*/
async createFolder(obj, args, context) {
async createAssetsFolder(obj, args, context) {
try {
const folderSlug = sanitize(args.slug).toLowerCase()
const parentFolderId = args.parentFolderId === 0 ? null : args.parentFolderId
......
......@@ -7,12 +7,6 @@ const graphHelper = require('../../helpers/graph')
module.exports = {
Query: {
async authentication () { return {} }
},
Mutation: {
async authentication () { return {} }
},
AuthenticationQuery: {
/**
* List of API Keys
*/
......@@ -34,7 +28,7 @@ module.exports = {
apiState () {
return WIKI.config.api.isEnabled
},
async strategies () {
async authStrategies () {
return WIKI.data.authentication.map(stg => ({
...stg,
isAvailable: stg.isAvailable === true,
......@@ -45,35 +39,35 @@ module.exports = {
})
}, []), 'key')
}))
},
/**
* Fetch active authentication strategies
*/
async activeStrategies (obj, args, context, info) {
let strategies = await WIKI.models.authentication.getStrategies()
strategies = strategies.map(stg => {
const strategyInfo = _.find(WIKI.data.authentication, ['key', stg.strategyKey]) || {}
return {
...stg,
strategy: strategyInfo,
config: _.sortBy(_.transform(stg.config, (res, value, key) => {
const configData = _.get(strategyInfo.props, key, false)
if (configData) {
res.push({
key,
value: JSON.stringify({
...configData,
value
})
})
}
}, []), 'key')
}
})
return args.enabledOnly ? _.filter(strategies, 'isEnabled') : strategies
}
// /**
// * Fetch active authentication strategies
// */
// async activeStrategies (obj, args, context, info) {
// let strategies = await WIKI.models.authentication.getStrategies()
// strategies = strategies.map(stg => {
// const strategyInfo = _.find(WIKI.data.authentication, ['key', stg.strategyKey]) || {}
// return {
// ...stg,
// strategy: strategyInfo,
// config: _.sortBy(_.transform(stg.config, (res, value, key) => {
// const configData = _.get(strategyInfo.props, key, false)
// if (configData) {
// res.push({
// key,
// value: JSON.stringify({
// ...configData,
// value
// })
// })
// }
// }, []), 'key')
// }
// })
// return args.enabledOnly ? _.filter(strategies, 'isEnabled') : strategies
// }
},
AuthenticationMutation: {
Mutation: {
/**
* Create New API Key
*/
......@@ -197,7 +191,7 @@ module.exports = {
/**
* Update Authentication Strategies
*/
async updateStrategies (obj, args, context) {
async updateAuthStrategies (obj, args, context) {
try {
const previousStrategies = await WIKI.models.authentication.getStrategies()
for (const str of args.strategies) {
......
......@@ -5,16 +5,10 @@ const graphHelper = require('../../helpers/graph')
module.exports = {
Query: {
async comments() { return {} }
},
Mutation: {
async comments() { return {} }
},
CommentQuery: {
/**
* Fetch list of Comments Providers
*/
async providers(obj, args, context, info) {
async commentsProviders(obj, args, context, info) {
const providers = await WIKI.models.commentProviders.getProviders()
return providers.map(provider => {
const providerInfo = _.find(WIKI.data.commentProviders, ['key', provider.key]) || {}
......@@ -39,7 +33,7 @@ module.exports = {
/**
* Fetch list of comments for a page
*/
async list (obj, args, context) {
async comments (obj, args, context) {
const page = await WIKI.models.pages.query().select('id').findOne({ localeCode: args.locale, path: args.path })
if (page) {
if (WIKI.auth.checkAccess(context.req.user, ['read:comments'], args)) {
......@@ -60,7 +54,7 @@ module.exports = {
/**
* Fetch a single comment
*/
async single (obj, args, context) {
async commentById (obj, args, context) {
const cm = await WIKI.data.commentProvider.getCommentById(args.id)
if (!cm || !cm.pageId) {
throw new WIKI.Error.CommentNotFound()
......@@ -86,11 +80,11 @@ module.exports = {
}
}
},
CommentMutation: {
Mutation: {
/**
* Create New Comment
*/
async create (obj, args, context) {
async createComment (obj, args, context) {
try {
const cmId = await WIKI.models.comments.postNewComment({
...args,
......@@ -108,7 +102,7 @@ module.exports = {
/**
* Update an Existing Comment
*/
async update (obj, args, context) {
async updateComment (obj, args, context) {
try {
const cmRender = await WIKI.models.comments.updateComment({
...args,
......@@ -126,7 +120,7 @@ module.exports = {
/**
* Delete an Existing Comment
*/
async delete (obj, args, context) {
async deleteComment (obj, args, context) {
try {
await WIKI.models.comments.deleteComment({
id: args.id,
......@@ -143,7 +137,7 @@ module.exports = {
/**
* Update Comments Providers
*/
async updateProviders(obj, args, context) {
async updateCommentsProviders(obj, args, context) {
try {
for (let provider of args.providers) {
await WIKI.models.commentProviders.query().patch({
......
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)
}
}
}
}
module.exports = {
// Query: {
// folders(obj, args, context, info) {
// return WIKI.models.Folder.findAll({ where: args })
// }
// },
// Mutation: {
// createFolder(obj, args) {
// return WIKI.models.Folder.create(args)
// },
// deleteFolder(obj, args) {
// return WIKI.models.Folder.destroy({
// where: {
// id: args.id
// },
// limit: 1
// })
// },
// renameFolder(obj, args) {
// return WIKI.models.Folder.update({
// name: args.name
// }, {
// where: { id: args.id }
// })
// }
// },
// Folder: {
// files(grp) {
// return grp.getFiles()
// }
// }
}
......@@ -7,16 +7,10 @@ const gql = require('graphql')
module.exports = {
Query: {
async groups () { return {} }
},
Mutation: {
async groups () { return {} }
},
GroupQuery: {
/**
* FETCH ALL GROUPS
*/
async list () {
async groups () {
return WIKI.models.groups.query().select(
'groups.*',
WIKI.models.groups.relatedQuery('users').count().as('userCount')
......@@ -25,15 +19,15 @@ module.exports = {
/**
* FETCH A SINGLE GROUP
*/
async single(obj, args) {
async groupById(obj, args) {
return WIKI.models.groups.query().findById(args.id)
}
},
GroupMutation: {
Mutation: {
/**
* ASSIGN USER TO GROUP
*/
async assignUser (obj, args, { req }) {
async assignUserToGroup (obj, args, { req }) {
// Check for guest user
if (args.userId === 2) {
throw new gql.GraphQLError('Cannot assign the Guest user to a group.')
......@@ -85,7 +79,7 @@ module.exports = {
/**
* CREATE NEW GROUP
*/
async create (obj, args, { req }) {
async createGroup (obj, args, { req }) {
const group = await WIKI.models.groups.query().insertAndFetch({
name: args.name,
permissions: JSON.stringify(WIKI.data.groups.defaultPermissions),
......@@ -102,7 +96,7 @@ module.exports = {
/**
* DELETE GROUP
*/
async delete (obj, args) {
async deleteGroup (obj, args) {
if (args.id === 1 || args.id === 2) {
throw new gql.GraphQLError('Cannot delete this group.')
}
......@@ -122,7 +116,7 @@ module.exports = {
/**
* UNASSIGN USER FROM GROUP
*/
async unassignUser (obj, args) {
async unassignUserFromGroup (obj, args) {
if (args.userId === 2) {
throw new gql.GraphQLError('Cannot unassign Guest user')
}
......@@ -149,7 +143,7 @@ module.exports = {
/**
* UPDATE GROUP
*/
async update (obj, args, { req }) {
async updateGroup (obj, args, { req }) {
// Check for unsafe regex page rules
if (_.some(args.pageRules, pr => {
return pr.match === 'REGEX' && !safeRegex(pr.path)
......
......@@ -5,12 +5,6 @@ const _ = require('lodash')
module.exports = {
Query: {
async localization() { return {} }
},
Mutation: {
async localization() { return {} }
},
LocalizationQuery: {
async locales(obj, args, context, info) {
let remoteLocales = await WIKI.cache.get('locales')
let localLocales = await WIKI.models.locales.query().select('code', 'isRTL', 'name', 'nativeName', 'createdAt', 'updatedAt', 'availability')
......@@ -24,19 +18,11 @@ module.exports = {
}
})
},
async config(obj, args, context, info) {
return {
locale: WIKI.config.lang.code,
autoUpdate: WIKI.config.lang.autoUpdate,
namespacing: WIKI.config.lang.namespacing,
namespaces: WIKI.config.lang.namespaces
}
},
translations (obj, args, context, info) {
return WIKI.lang.getByNamespace(args.locale, args.namespace)
}
},
LocalizationMutation: {
Mutation: {
async downloadLocale(obj, args, context) {
try {
const job = await WIKI.scheduler.registerJob({
......
......@@ -5,21 +5,15 @@ const graphHelper = require('../../helpers/graph')
module.exports = {
Query: {
async mail() { return {} }
},
Mutation: {
async mail() { return {} }
},
MailQuery: {
async config(obj, args, context, info) {
async mailConfig(obj, args, context, info) {
return {
...WIKI.config.mail,
pass: WIKI.config.mail.pass.length > 0 ? '********' : ''
}
}
},
MailMutation: {
async sendTest(obj, args, context) {
Mutation: {
async sendMailTest(obj, args, context) {
try {
if (_.isEmpty(args.recipientEmail) || args.recipientEmail.length < 6) {
throw new WIKI.Error.MailInvalidRecipient()
......@@ -42,7 +36,7 @@ module.exports = {
return graphHelper.generateError(err)
}
},
async updateConfig(obj, args, context) {
async updateMailConfig(obj, args, context) {
try {
WIKI.config.mail = {
senderName: args.senderName,
......
......@@ -4,21 +4,15 @@ const graphHelper = require('../../helpers/graph')
module.exports = {
Query: {
async navigation () { return {} }
},
Mutation: {
async navigation () { return {} }
},
NavigationQuery: {
async tree (obj, args, context, info) {
async navigationTree (obj, args, context, info) {
return WIKI.models.navigation.getTree({ cache: false, locale: 'all', bypassAuth: true })
},
config (obj, args, context, info) {
navigationConfig (obj, args, context, info) {
return WIKI.config.nav
}
},
NavigationMutation: {
async updateTree (obj, args, context) {
Mutation: {
async updateNavigationTree (obj, args, context) {
try {
await WIKI.models.navigation.query().patch({
config: args.tree
......@@ -34,7 +28,7 @@ module.exports = {
return graphHelper.generateError(err)
}
},
async updateConfig (obj, args, context) {
async updateNavigationConfig (obj, args, context) {
try {
WIKI.config.nav = {
mode: args.mode
......
......@@ -5,16 +5,10 @@ const graphHelper = require('../../helpers/graph')
module.exports = {
Query: {
async pages() { return {} }
},
Mutation: {
async pages() { return {} }
},
PageQuery: {
/**
* PAGE HISTORY
*/
async history(obj, args, context, info) {
async pageHistoryById (obj, args, context, info) {
const page = await WIKI.models.pages.query().select('path', 'localeCode').findById(args.id)
if (WIKI.auth.checkAccess(context.req.user, ['read:history'], {
path: page.path,
......@@ -32,7 +26,7 @@ module.exports = {
/**
* PAGE VERSION
*/
async version(obj, args, context, info) {
async pageVersionById (obj, args, context, info) {
const page = await WIKI.models.pages.query().select('path', 'localeCode').findById(args.pageId)
if (WIKI.auth.checkAccess(context.req.user, ['read:history'], {
path: page.path,
......@@ -49,7 +43,7 @@ module.exports = {
/**
* SEARCH PAGES
*/
async search (obj, args, context) {
async searchPages (obj, args, context) {
if (WIKI.data.searchEngine) {
const resp = await WIKI.data.searchEngine.query(args.query, args)
return {
......@@ -73,7 +67,7 @@ module.exports = {
/**
* LIST PAGES
*/
async list (obj, args, context, info) {
async pages (obj, args, context, info) {
let results = await WIKI.models.pages.query().column([
'pages.id',
'path',
......@@ -149,7 +143,7 @@ module.exports = {
/**
* FETCH SINGLE PAGE
*/
async single (obj, args, context, info) {
async pageById (obj, args, context, info) {
let page = await WIKI.models.pages.getPageFromDb(args.id)
if (page) {
if (WIKI.auth.checkAccess(context.req.user, ['manage:pages', 'delete:pages'], {
......@@ -220,7 +214,7 @@ module.exports = {
/**
* FETCH PAGE TREE
*/
async tree (obj, args, context, info) {
async pageTree (obj, args, context, info) {
let curPage = null
if (!args.locale) { args.locale = WIKI.config.lang.code }
......@@ -270,7 +264,7 @@ module.exports = {
/**
* FETCH PAGE LINKS
*/
async links (obj, args, context, info) {
async pageLinks (obj, args, context, info) {
let results
if (WIKI.config.db.type === 'mysql' || WIKI.config.db.type === 'mariadb' || WIKI.config.db.type === 'sqlite') {
......@@ -343,7 +337,7 @@ module.exports = {
/**
* FETCH LATEST VERSION FOR CONFLICT COMPARISON
*/
async conflictLatest (obj, args, context, info) {
async checkConflictsLatest (obj, args, context, info) {
let page = await WIKI.models.pages.getPageFromDb(args.id)
if (page) {
if (WIKI.auth.checkAccess(context.req.user, ['write:pages', 'manage:pages'], {
......@@ -363,11 +357,11 @@ module.exports = {
}
}
},
PageMutation: {
Mutation: {
/**
* CREATE PAGE
*/
async create(obj, args, context) {
async createPage(obj, args, context) {
try {
const page = await WIKI.models.pages.createPage({
...args,
......@@ -384,7 +378,7 @@ module.exports = {
/**
* UPDATE PAGE
*/
async update(obj, args, context) {
async updatePage(obj, args, context) {
try {
const page = await WIKI.models.pages.updatePage({
...args,
......@@ -401,7 +395,7 @@ module.exports = {
/**
* CONVERT PAGE
*/
async convert(obj, args, context) {
async convertPage(obj, args, context) {
try {
await WIKI.models.pages.convertPage({
...args,
......@@ -415,9 +409,9 @@ module.exports = {
}
},
/**
* MOVE PAGE
* RENAME PAGE
*/
async move(obj, args, context) {
async renamePage(obj, args, context) {
try {
await WIKI.models.pages.movePage({
...args,
......@@ -433,7 +427,7 @@ module.exports = {
/**
* DELETE PAGE
*/
async delete(obj, args, context) {
async deletePage(obj, args, context) {
try {
await WIKI.models.pages.deletePage({
...args,
......@@ -517,7 +511,7 @@ module.exports = {
/**
* REBUILD TREE
*/
async rebuildTree(obj, args, context) {
async rebuildPageTree(obj, args, context) {
try {
await WIKI.models.pages.rebuildTree()
return {
......@@ -530,7 +524,7 @@ module.exports = {
/**
* RENDER PAGE
*/
async render (obj, args, context) {
async renderPage (obj, args, context) {
try {
const page = await WIKI.models.pages.query().findById(args.id)
if (!page) {
......@@ -547,7 +541,7 @@ module.exports = {
/**
* RESTORE PAGE VERSION
*/
async restore (obj, args, context) {
async restorePage (obj, args, context) {
try {
const page = await WIKI.models.pages.query().select('path', 'localeCode').findById(args.pageId)
if (!page) {
......@@ -583,7 +577,7 @@ module.exports = {
/**
* Purge history
*/
async purgeHistory (obj, args, context) {
async purgePagesHistory (obj, args, context) {
try {
await WIKI.models.pageHistory.purge(args.olderThan)
return {
......
......@@ -5,12 +5,6 @@ const graphHelper = require('../../helpers/graph')
module.exports = {
Query: {
async rendering() { return {} }
},
Mutation: {
async rendering() { return {} }
},
RenderingQuery: {
async renderers(obj, args, context, info) {
let renderers = await WIKI.models.renderers.getRenderers()
renderers = renderers.map(rdr => {
......@@ -37,7 +31,7 @@ module.exports = {
return renderers
}
},
RenderingMutation: {
Mutation: {
async updateRenderers(obj, args, context) {
try {
for (let rdr of args.renderers) {
......
const _ = require('lodash')
const graphHelper = require('../../helpers/graph')
/* global WIKI */
module.exports = {
Query: {
async search() { return {} }
},
Mutation: {
async search() { return {} }
},
SearchQuery: {
async searchEngines(obj, args, context, info) {
let searchEngines = await WIKI.models.searchEngines.getSearchEngines()
searchEngines = searchEngines.map(searchEngine => {
const searchEngineInfo = _.find(WIKI.data.searchEngines, ['key', searchEngine.key]) || {}
return {
...searchEngineInfo,
...searchEngine,
config: _.sortBy(_.transform(searchEngine.config, (res, value, key) => {
const configData = _.get(searchEngineInfo.props, key, false)
if (configData) {
res.push({
key,
value: JSON.stringify({
...configData,
value
})
})
}
}, []), 'key')
}
})
// if (args.filter) { searchEngines = graphHelper.filter(searchEngines, args.filter) }
if (args.orderBy) { searchEngines = _.sortBy(searchEngines, [args.orderBy]) }
return searchEngines
}
},
SearchMutation: {
async updateSearchEngines(obj, args, context) {
try {
let newActiveEngine = ''
for (let searchEngine of args.engines) {
if (searchEngine.isEnabled) {
newActiveEngine = searchEngine.key
}
await WIKI.models.searchEngines.query().patch({
isEnabled: searchEngine.isEnabled,
config: _.reduce(searchEngine.config, (result, value, key) => {
_.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null))
return result
}, {})
}).where('key', searchEngine.key)
}
if (newActiveEngine !== WIKI.data.searchEngine.key) {
try {
await WIKI.data.searchEngine.deactivate()
} catch (err) {
WIKI.logger.warn('Failed to deactivate previous search engine:', err)
}
}
await WIKI.models.searchEngines.initEngine({ activate: true })
return {
responseResult: graphHelper.generateSuccess('Search Engines updated successfully')
}
} catch (err) {
return graphHelper.generateError(err)
}
},
async rebuildIndex (obj, args, context) {
async rebuildSearchIndex (obj, args, context) {
try {
await WIKI.data.searchEngine.rebuild()
return {
......
......@@ -40,9 +40,7 @@ module.exports = {
hostname: site.hostname,
isEnabled: site.isEnabled
} : null
},
// LEGACY
async site() { return {} }
}
},
Mutation: {
/**
......@@ -178,118 +176,6 @@ module.exports = {
return {
status: graphHelper.generateSuccess('Site favicon uploaded successfully')
}
},
// LEGACY
async site() { return {} }
},
SiteQuery: {
async config(obj, args, context, info) {
return {
host: WIKI.config.host,
title: WIKI.config.title,
company: WIKI.config.company,
contentLicense: WIKI.config.contentLicense,
logoUrl: WIKI.config.logoUrl,
...WIKI.config.seo,
...WIKI.config.features,
...WIKI.config.security,
authAutoLogin: WIKI.config.auth.autoLogin,
authEnforce2FA: WIKI.config.auth.enforce2FA,
authHideLocal: WIKI.config.auth.hideLocal,
authLoginBgUrl: WIKI.config.auth.loginBgUrl,
authJwtAudience: WIKI.config.auth.audience,
authJwtExpiration: WIKI.config.auth.tokenExpiration,
authJwtRenewablePeriod: WIKI.config.auth.tokenRenewal,
uploadMaxFileSize: WIKI.config.uploads.maxFileSize,
uploadMaxFiles: WIKI.config.uploads.maxFiles,
uploadScanSVG: WIKI.config.uploads.scanSVG,
uploadForceDownload: WIKI.config.uploads.forceDownload
}
}
},
SiteMutation: {
async updateConfig(obj, args, context) {
try {
if (args.host) {
let siteHost = _.trim(args.host)
if (siteHost.endsWith('/')) {
siteHost = siteHost.slice(0, -1)
}
WIKI.config.host = siteHost
}
if (args.title) {
WIKI.config.title = _.trim(args.title)
}
if (args.company) {
WIKI.config.company = _.trim(args.company)
}
if (args.contentLicense) {
WIKI.config.contentLicense = args.contentLicense
}
if (args.logoUrl) {
WIKI.config.logoUrl = _.trim(args.logoUrl)
}
WIKI.config.seo = {
description: _.get(args, 'description', WIKI.config.seo.description),
robots: _.get(args, 'robots', WIKI.config.seo.robots),
analyticsService: _.get(args, 'analyticsService', WIKI.config.seo.analyticsService),
analyticsId: _.get(args, 'analyticsId', WIKI.config.seo.analyticsId)
}
WIKI.config.auth = {
autoLogin: _.get(args, 'authAutoLogin', WIKI.config.auth.autoLogin),
enforce2FA: _.get(args, 'authEnforce2FA', WIKI.config.auth.enforce2FA),
hideLocal: _.get(args, 'authHideLocal', WIKI.config.auth.hideLocal),
loginBgUrl: _.get(args, 'authLoginBgUrl', WIKI.config.auth.loginBgUrl),
audience: _.get(args, 'authJwtAudience', WIKI.config.auth.audience),
tokenExpiration: _.get(args, 'authJwtExpiration', WIKI.config.auth.tokenExpiration),
tokenRenewal: _.get(args, 'authJwtRenewablePeriod', WIKI.config.auth.tokenRenewal)
}
WIKI.config.features = {
featurePageRatings: _.get(args, 'featurePageRatings', WIKI.config.features.featurePageRatings),
featurePageComments: _.get(args, 'featurePageComments', WIKI.config.features.featurePageComments),
featurePersonalWikis: _.get(args, 'featurePersonalWikis', WIKI.config.features.featurePersonalWikis)
}
WIKI.config.security = {
securityOpenRedirect: _.get(args, 'securityOpenRedirect', WIKI.config.security.securityOpenRedirect),
securityIframe: _.get(args, 'securityIframe', WIKI.config.security.securityIframe),
securityReferrerPolicy: _.get(args, 'securityReferrerPolicy', WIKI.config.security.securityReferrerPolicy),
securityTrustProxy: _.get(args, 'securityTrustProxy', WIKI.config.security.securityTrustProxy),
securitySRI: _.get(args, 'securitySRI', WIKI.config.security.securitySRI),
securityHSTS: _.get(args, 'securityHSTS', WIKI.config.security.securityHSTS),
securityHSTSDuration: _.get(args, 'securityHSTSDuration', WIKI.config.security.securityHSTSDuration),
securityCSP: _.get(args, 'securityCSP', WIKI.config.security.securityCSP),
securityCSPDirectives: _.get(args, 'securityCSPDirectives', WIKI.config.security.securityCSPDirectives)
}
WIKI.config.uploads = {
maxFileSize: _.get(args, 'uploadMaxFileSize', WIKI.config.uploads.maxFileSize),
maxFiles: _.get(args, 'uploadMaxFiles', WIKI.config.uploads.maxFiles),
scanSVG: _.get(args, 'uploadScanSVG', WIKI.config.uploads.scanSVG),
forceDownload: _.get(args, 'uploadForceDownload', WIKI.config.uploads.forceDownload)
}
await WIKI.configSvc.saveToDb(['host', 'title', 'company', 'contentLicense', 'seo', 'logoUrl', 'auth', 'features', 'security', 'uploads'])
if (WIKI.config.security.securityTrustProxy) {
WIKI.app.enable('trust proxy')
} else {
WIKI.app.disable('trust proxy')
}
return {
responseResult: graphHelper.generateSuccess('Site configuration updated successfully')
}
} catch (err) {
return graphHelper.generateError(err)
}
}
}
}
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()
// }
// }
}
const graphHelper = require('../../helpers/graph')
const _ = require('lodash')
const CleanCSS = require('clean-css')
/* global WIKI */
module.exports = {
Query: {
async theming() { return {} }
},
Mutation: {
async theming() { return {} }
},
ThemingQuery: {
async themes(obj, args, context, info) {
return [{ // TODO
key: 'default',
title: 'Default',
author: 'requarks.io'
}]
},
async config(obj, args, context, info) {
return {
theme: WIKI.config.theming.theme,
iconset: WIKI.config.theming.iconset,
darkMode: WIKI.config.theming.darkMode,
injectCSS: new CleanCSS({ format: 'beautify' }).minify(WIKI.config.theming.injectCSS).styles,
injectHead: WIKI.config.theming.injectHead,
injectBody: WIKI.config.theming.injectBody
}
}
},
ThemingMutation: {
async setConfig(obj, args, context, info) {
try {
if (!_.isEmpty(args.injectCSS)) {
args.injectCSS = new CleanCSS({
inline: false
}).minify(args.injectCSS).styles
}
WIKI.config.theming = {
...WIKI.config.theming,
theme: args.theme,
iconset: args.iconset,
darkMode: args.darkMode,
injectCSS: args.injectCSS || '',
injectHead: args.injectHead || '',
injectBody: args.injectBody || ''
}
await WIKI.configSvc.saveToDb(['theming'])
return {
responseResult: graphHelper.generateSuccess('Theme config updated')
}
} catch (err) {
return graphHelper.generateError(err)
}
}
}
}
......@@ -5,32 +5,54 @@ const _ = require('lodash')
module.exports = {
Query: {
async users() { return {} }
},
Mutation: {
async users() { return {} }
},
UserQuery: {
async list(obj, args, context, info) {
return WIKI.models.users.query()
.select('id', 'email', 'name', 'providerKey', 'isSystem', 'isActive', 'createdAt', 'lastLoginAt')
},
async search(obj, args, context, info) {
/**
* FETCH ALL USERS
*/
async users (obj, args, context, info) {
// -> Sanitize limit
let limit = args.pageSize ?? 20
if (limit < 1 || limit > 1000) {
limit = 1000
}
// -> Sanitize offset
let offset = args.page ?? 1
if (offset < 1) {
offset = 1
}
// -> Fetch Users
return WIKI.models.users.query()
.where('email', 'like', `%${args.query}%`)
.orWhere('name', 'like', `%${args.query}%`)
.limit(10)
.select('id', 'email', 'name', 'providerKey', 'createdAt')
.select('id', 'email', 'name', 'isSystem', 'isActive', 'createdAt', 'lastLoginAt')
.where(builder => {
if (args.filter) {
builder.where('email', 'like', `%${args.filter}%`)
.orWhere('name', 'like', `%${args.filter}%`)
}
})
.orderBy(args.orderBy ?? 'name', args.orderByDirection ?? 'asc')
.offset((offset - 1) * limit)
.limit(limit)
},
async single(obj, args, context, info) {
let usr = await WIKI.models.users.query().findById(args.id)
usr.password = ''
usr.tfaSecret = ''
/**
* FETCH A SINGLE USER
*/
async userById (obj, args, context, info) {
const usr = await WIKI.models.users.query().findById(args.id)
// const str = _.get(WIKI.auth.strategies, usr.providerKey)
// str.strategy = _.find(WIKI.data.authentication, ['key', str.strategyKey])
// usr.providerName = str.displayName
// usr.providerIs2FACapable = _.get(str, 'strategy.useForm', false)
const str = _.get(WIKI.auth.strategies, usr.providerKey)
str.strategy = _.find(WIKI.data.authentication, ['key', str.strategyKey])
usr.providerName = str.displayName
usr.providerIs2FACapable = _.get(str, 'strategy.useForm', false)
usr.auth = _.mapValues(usr.auth, (auth, providerKey) => {
if (auth.password) {
auth.password = '***'
}
auth.module = providerKey === '00910749-8ab6-498a-9be0-f4ca28ea5e52' ? 'google' : 'local'
auth._moduleName = providerKey === '00910749-8ab6-498a-9be0-f4ca28ea5e52' ? 'Google' : 'Local'
return auth
})
return usr
},
......@@ -61,19 +83,19 @@ module.exports = {
.limit(10)
}
},
UserMutation: {
async create (obj, args) {
Mutation: {
async createUser (obj, args) {
try {
await WIKI.models.users.createNewUser(args)
await WIKI.models.users.createNewUser({ ...args, passwordRaw: args.password, isVerified: true })
return {
responseResult: graphHelper.generateSuccess('User created successfully')
status: graphHelper.generateSuccess('User created successfully')
}
} catch (err) {
return graphHelper.generateError(err)
}
},
async delete (obj, args) {
async deleteUser (obj, args) {
try {
if (args.id <= 2) {
throw new WIKI.Error.UserDeleteProtected()
......@@ -84,7 +106,7 @@ module.exports = {
WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })
return {
responseResult: graphHelper.generateSuccess('User deleted successfully')
status: graphHelper.generateSuccess('User deleted successfully')
}
} catch (err) {
if (err.message.indexOf('foreign') >= 0) {
......@@ -94,40 +116,40 @@ module.exports = {
}
}
},
async update (obj, args) {
async updateUser (obj, args) {
try {
await WIKI.models.users.updateUser(args)
await WIKI.models.users.updateUser(args.id, args.patch)
return {
responseResult: graphHelper.generateSuccess('User created successfully')
status: graphHelper.generateSuccess('User updated successfully')
}
} catch (err) {
return graphHelper.generateError(err)
}
},
async verify (obj, args) {
async verifyUser (obj, args) {
try {
await WIKI.models.users.query().patch({ isVerified: true }).findById(args.id)
return {
responseResult: graphHelper.generateSuccess('User verified successfully')
status: graphHelper.generateSuccess('User verified successfully')
}
} catch (err) {
return graphHelper.generateError(err)
}
},
async activate (obj, args) {
async activateUser (obj, args) {
try {
await WIKI.models.users.query().patch({ isActive: true }).findById(args.id)
return {
responseResult: graphHelper.generateSuccess('User activated successfully')
status: graphHelper.generateSuccess('User activated successfully')
}
} catch (err) {
return graphHelper.generateError(err)
}
},
async deactivate (obj, args) {
async deactivateUser (obj, args) {
try {
if (args.id <= 2) {
throw new Error('Cannot deactivate system accounts.')
......@@ -138,35 +160,35 @@ module.exports = {
WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })
return {
responseResult: graphHelper.generateSuccess('User deactivated successfully')
status: graphHelper.generateSuccess('User deactivated successfully')
}
} catch (err) {
return graphHelper.generateError(err)
}
},
async enableTFA (obj, args) {
async enableUserTFA (obj, args) {
try {
await WIKI.models.users.query().patch({ tfaIsActive: true, tfaSecret: null }).findById(args.id)
return {
responseResult: graphHelper.generateSuccess('User 2FA enabled successfully')
status: graphHelper.generateSuccess('User 2FA enabled successfully')
}
} catch (err) {
return graphHelper.generateError(err)
}
},
async disableTFA (obj, args) {
async disableUserTFA (obj, args) {
try {
await WIKI.models.users.query().patch({ tfaIsActive: false, tfaSecret: null }).findById(args.id)
return {
responseResult: graphHelper.generateSuccess('User 2FA disabled successfully')
status: graphHelper.generateSuccess('User 2FA disabled successfully')
}
} catch (err) {
return graphHelper.generateError(err)
}
},
resetPassword (obj, args) {
resetUserPassword (obj, args) {
return false
},
async updateProfile (obj, args, context) {
......@@ -203,7 +225,7 @@ module.exports = {
const newToken = await WIKI.models.users.refreshToken(usr.id)
return {
responseResult: graphHelper.generateSuccess('User profile updated successfully'),
status: graphHelper.generateSuccess('User profile updated successfully'),
jwt: newToken.token
}
} catch (err) {
......
const gql = require('graphql')
module.exports = {
Date: new gql.GraphQLScalarType({
name: 'Date',
description: 'ISO date-time string at UTC',
parseValue(value) {
return new Date(value)
},
serialize(value) {
return value.toISOString()
},
parseLiteral(ast) {
if (ast.kind !== gql.Kind.STRING) {
throw new TypeError('Date value must be an string!')
}
return new Date(ast.value)
module.exports = new gql.GraphQLScalarType({
name: 'Date',
description: 'ISO date-time string at UTC',
parseValue(value) {
return new Date(value)
},
serialize(value) {
return value.toISOString()
},
parseLiteral(ast) {
if (ast.kind !== gql.Kind.STRING) {
throw new TypeError('Date value must be an string!')
}
})
}
return new Date(ast.value)
}
})
......@@ -39,21 +39,19 @@ function parseObject (typeName, ast, variables) {
return value
}
module.exports = {
JSON: new GraphQLScalarType({
name: 'JSON',
description:
'The `JSON` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).',
specifiedByUrl:
'http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf',
serialize: ensureObject,
parseValue: ensureObject,
parseLiteral: (ast, variables) => {
if (ast.kind !== Kind.OBJECT) {
throw new TypeError(`JSONObject cannot represent non-object value: ${ast}`)
}
return parseObject('JSONObject', ast, variables)
module.exports = new GraphQLScalarType({
name: 'JSON',
description:
'The `JSON` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).',
specifiedByUrl:
'http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf',
serialize: ensureObject,
parseValue: ensureObject,
parseLiteral: (ast, variables) => {
if (ast.kind !== Kind.OBJECT) {
throw new TypeError(`JSONObject cannot represent non-object value: ${ast}`)
}
})
}
return parseObject('JSONObject', ast, variables)
}
})
const { Kind, GraphQLScalarType } = require('graphql')
// const { Kind } = require('graphql/language')
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
const nilUUID = '00000000-0000-0000-0000-000000000000'
......@@ -8,32 +7,30 @@ function isUUID (value) {
return uuidRegex.test(value) || nilUUID === value
}
module.exports = {
UUID: new GraphQLScalarType({
name: 'UUID',
description: 'The `UUID` scalar type represents UUID values as specified by [RFC 4122](https://tools.ietf.org/html/rfc4122).',
serialize: (value) => {
if (!isUUID(value)) {
throw new TypeError(`UUID cannot represent non-UUID value: ${value}`)
}
module.exports = new GraphQLScalarType({
name: 'UUID',
description: 'The `UUID` scalar type represents UUID values as specified by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122).',
serialize: (value) => {
if (!isUUID(value)) {
throw new TypeError(`UUID cannot represent non-UUID value: ${value}`)
}
return value.toLowerCase()
},
parseValue: (value) => {
if (!isUUID(value)) {
throw new TypeError(`UUID cannot represent non-UUID value: ${value}`)
}
return value.toLowerCase()
},
parseValue: (value) => {
if (!isUUID(value)) {
throw new TypeError(`UUID cannot represent non-UUID value: ${value}`)
}
return value.toLowerCase()
},
parseLiteral: (ast) => {
if (ast.kind === Kind.STRING) {
if (isUUID(ast.value)) {
return ast.value
}
return value.toLowerCase()
},
parseLiteral: (ast) => {
if (ast.kind === Kind.STRING) {
if (isUUID(ast.value)) {
return ast.value
}
return undefined
}
})
}
return undefined
}
})
......@@ -3,45 +3,23 @@
# ===============================================
extend type Query {
analytics: AnalyticsQuery
}
extend type Mutation {
analytics: AnalyticsMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
"""
Queries for Analytics
"""
type AnalyticsQuery {
"""
Fetch list of Analytics providers and their configuration
"""
providers(
analyticsProviders(
"Return only active providers"
isEnabled: Boolean
): [AnalyticsProvider] @auth(requires: ["manage:system"])
): [AnalyticsProvider]
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
"""
Mutations for Analytics
"""
type AnalyticsMutation {
extend type Mutation {
"""
Update a list of Analytics providers and their configuration
"""
updateProviders(
updateAnalyticsProviders(
"List of providers"
providers: [AnalyticsProviderInput]!
): DefaultResponse @auth(requires: ["manage:system"])
): DefaultResponse
}
# -----------------------------------------------
......
......@@ -3,49 +3,33 @@
# ===============================================
extend type Query {
assets: AssetQuery
}
extend type Mutation {
assets: AssetMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type AssetQuery {
list(
assets(
folderId: Int!
kind: AssetKind!
): [AssetItem] @auth(requires: ["manage:system", "read:assets"])
): [AssetItem]
folders(
assetsFolders(
parentFolderId: Int!
): [AssetFolder] @auth(requires: ["manage:system", "read:assets"])
): [AssetFolder]
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type AssetMutation {
createFolder(
extend type Mutation {
createAssetsFolder(
parentFolderId: Int!
slug: String!
name: String
): DefaultResponse @auth(requires: ["manage:system", "write:assets"])
): DefaultResponse
renameAsset(
id: Int!
filename: String!
): DefaultResponse @auth(requires: ["manage:system", "manage:assets"])
): DefaultResponse
deleteAsset(
id: Int!
): DefaultResponse @auth(requires: ["manage:system", "manage:assets"])
): DefaultResponse
flushTempUploads: DefaultResponse @auth(requires: ["manage:system"])
flushTempUploads: DefaultResponse
}
# -----------------------------------------------
......
......@@ -3,40 +3,22 @@
# ===============================================
extend type Query {
authentication: AuthenticationQuery
}
extend type Mutation {
authentication: AuthenticationMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
apiKeys: [AuthenticationApiKey]
type AuthenticationQuery {
apiKeys: [AuthenticationApiKey] @auth(requires: ["manage:system", "manage:api"])
apiState: Boolean
apiState: Boolean! @auth(requires: ["manage:system", "manage:api"])
strategies: [AuthenticationStrategy] @auth(requires: ["manage:system"])
activeStrategies(
authStrategies(
enabledOnly: Boolean
): [AuthenticationActiveStrategy]
): [AuthenticationStrategy]
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type AuthenticationMutation {
extend type Mutation {
createApiKey(
name: String!
expiration: String!
fullAccess: Boolean!
group: Int
): AuthenticationCreateApiKeyResponse @auth(requires: ["manage:system", "manage:api"])
): AuthenticationCreateApiKeyResponse
login(
username: String!
......@@ -67,19 +49,19 @@ type AuthenticationMutation {
revokeApiKey(
id: Int!
): DefaultResponse @auth(requires: ["manage:system", "manage:api"])
): DefaultResponse
setApiState(
enabled: Boolean!
): DefaultResponse @auth(requires: ["manage:system", "manage:api"])
): DefaultResponse
updateStrategies(
updateAuthStrategies(
strategies: [AuthenticationStrategyInput]!
): DefaultResponse @auth(requires: ["manage:system"])
): DefaultResponse
regenerateCertificates: DefaultResponse @auth(requires: ["manage:system"])
regenerateCertificates: DefaultResponse
resetGuestUser: DefaultResponse @auth(requires: ["manage:system"])
resetGuestUser: DefaultResponse
}
# -----------------------------------------------
......@@ -113,7 +95,7 @@ type AuthenticationActiveStrategy {
}
type AuthenticationLoginResponse {
responseResult: ResponseStatus
operation: Operation
jwt: String
mustChangePwd: Boolean
mustProvideTFA: Boolean
......@@ -124,7 +106,7 @@ type AuthenticationLoginResponse {
}
type AuthenticationRegisterResponse {
responseResult: ResponseStatus
operation: Operation
jwt: String
}
......@@ -151,6 +133,6 @@ type AuthenticationApiKey {
}
type AuthenticationCreateApiKeyResponse {
responseResult: ResponseStatus
operation: Operation
key: String
}
......@@ -3,55 +3,39 @@
# ===============================================
extend type Query {
comments: CommentQuery
}
extend type Mutation {
comments: CommentMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type CommentQuery {
providers: [CommentProvider] @auth(requires: ["manage:system"])
commentsProviders: [CommentProvider]
list(
comments(
locale: String!
path: String!
): [CommentPost]! @auth(requires: ["read:comments", "manage:system"])
): [CommentPost]!
single(
commentById(
id: Int!
): CommentPost @auth(requires: ["read:comments", "manage:system"])
): CommentPost
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type CommentMutation {
updateProviders(
extend type Mutation {
updateCommentsProviders(
providers: [CommentProviderInput]
): DefaultResponse @auth(requires: ["manage:system"])
): DefaultResponse
create(
createComment(
pageId: Int!
replyTo: Int
content: String!
guestName: String
guestEmail: String
): CommentCreateResponse @auth(requires: ["write:comments", "manage:system"]) @rateLimit(limit: 1, duration: 15)
): CommentCreateResponse @rateLimit(limit: 1, duration: 15)
update(
updateComment(
id: Int!
content: String!
): CommentUpdateResponse @auth(requires: ["write:comments", "manage:comments", "manage:system"])
): CommentUpdateResponse
delete(
deleteComment(
id: Int!
): DefaultResponse @auth(requires: ["manage:comments", "manage:system"])
): DefaultResponse
}
# -----------------------------------------------
......@@ -59,9 +43,9 @@ type CommentMutation {
# -----------------------------------------------
type CommentProvider {
isEnabled: Boolean!
key: String!
title: String!
isEnabled: Boolean
key: String
title: String
description: String
logo: String
website: String
......@@ -76,23 +60,23 @@ input CommentProviderInput {
}
type CommentPost {
id: Int!
content: String! @auth(requires: ["write:comments", "manage:comments", "manage:system"])
render: String!
authorId: Int!
authorName: String!
authorEmail: String! @auth(requires: ["manage:system"])
authorIP: String! @auth(requires: ["manage:system"])
createdAt: Date!
updatedAt: Date!
id: Int
content: String
render: String
authorId: Int
authorName: String
authorEmail: String
authorIP: String
createdAt: Date
updatedAt: Date
}
type CommentCreateResponse {
responseResult: ResponseStatus
operation: Operation
id: Int
}
type CommentUpdateResponse {
responseResult: ResponseStatus
operation: Operation
render: String
}
......@@ -2,7 +2,7 @@
# Wiki.js GraphQL Schema #
# ====================== #
# DIRECTIVES
# DIRECTIVES (deprecated)
# ----------
directive @auth(requires: [String]) on QUERY | FIELD_DEFINITION | ARGUMENT_DEFINITION
......@@ -12,8 +12,8 @@ directive @auth(requires: [String]) on QUERY | FIELD_DEFINITION | ARGUMENT_DEFIN
# Generic Key Value Pair
type KeyValuePair {
key: String!
value: String!
key: String
value: String
}
# General Key Value Pair Input
input KeyValuePairInput {
......@@ -23,14 +23,13 @@ input KeyValuePairInput {
# Generic Mutation Response
type DefaultResponse {
responseResult: ResponseStatus
operation: Operation
}
# Mutation Status
type ResponseStatus {
succeeded: Boolean!
errorCode: Int!
slug: String!
# Mutation Operation
type Operation {
succeeded: Boolean
slug: String
message: String
}
......@@ -47,6 +46,3 @@ type Query
# Mutations (Create, Update, Delete)
type Mutation
# Subscriptions (Push, Real-time)
type Subscription
# ===============================================
# CONTRIBUTE
# ===============================================
extend type Query {
contribute: ContributeQuery
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type ContributeQuery {
contributors: [ContributeContributor]
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type ContributeContributor {
id: String!
source: String!
name: String!
joined: Date!
website: String
twitter: String
avatar: String
}
......@@ -3,58 +3,39 @@
# ===============================================
extend type Query {
groups: GroupQuery
}
extend type Mutation {
groups: GroupMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type GroupQuery {
list(
groups(
filter: String
orderBy: String
): [GroupMinimal] @auth(requires: ["write:groups", "manage:groups", "manage:system"])
): [Group]
single(
groupById(
id: Int!
): Group @auth(requires: ["write:groups", "manage:groups", "manage:system"])
): Group
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type GroupMutation {
create(
extend type Mutation {
createGroup(
name: String!
): GroupResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"])
): GroupResponse
update(
updateGroup(
id: Int!
name: String!
redirectOnLogin: String!
permissions: [String]!
pageRules: [PageRuleInput]!
): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"])
patch: GroupUpdateInput!
): DefaultResponse
delete(
deleteGroup(
id: Int!
): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"])
): DefaultResponse
assignUser(
assignUserToGroup(
groupId: Int!
userId: Int!
): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"])
): DefaultResponse
unassignUser(
unassignUserFromGroup(
groupId: Int!
userId: Int!
): DefaultResponse @auth(requires: ["write:groups", "manage:groups", "manage:system"])
): DefaultResponse
}
# -----------------------------------------------
......@@ -62,38 +43,36 @@ type GroupMutation {
# -----------------------------------------------
type GroupResponse {
responseResult: ResponseStatus!
operation: Operation
group: Group
}
type GroupMinimal {
id: Int!
name: String!
isSystem: Boolean!
userCount: Int
createdAt: Date!
updatedAt: Date!
}
type Group {
id: Int!
name: String!
isSystem: Boolean!
id: Int
name: String
isSystem: Boolean
redirectOnLogin: String
permissions: [String]!
permissions: [String]
pageRules: [PageRule]
users: [UserMinimal]
createdAt: Date!
updatedAt: Date!
createdAt: Date
updatedAt: Date
}
type PageRule {
id: String!
deny: Boolean!
match: PageRuleMatch!
roles: [String]!
path: String!
locales: [String]!
id: String
deny: Boolean
match: PageRuleMatch
roles: [String]
path: String
locales: [String]
}
input GroupUpdateInput {
name: String!
redirectOnLogin: String!
permissions: [String]!
pageRules: [PageRuleInput]!
}
input PageRuleInput {
......
......@@ -3,38 +3,21 @@
# ===============================================
extend type Query {
localization: LocalizationQuery
}
extend type Mutation {
localization: LocalizationMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type LocalizationQuery {
locales: [LocalizationLocale]
config: LocalizationConfig
translations(locale: String!, namespace: String!): [Translation]
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type LocalizationMutation {
extend type Mutation {
downloadLocale(
locale: String!
): DefaultResponse @auth(requires: ["manage:system"])
): DefaultResponse
updateLocale(
locale: String!
autoUpdate: Boolean!
namespacing: Boolean!
namespaces: [String]!
): DefaultResponse @auth(requires: ["manage:system"])
): DefaultResponse
}
# -----------------------------------------------
......@@ -42,25 +25,18 @@ type LocalizationMutation {
# -----------------------------------------------
type LocalizationLocale {
availability: Int!
code: String!
createdAt: Date!
availability: Int
code: String
createdAt: Date
installDate: Date
isInstalled: Boolean!
isRTL: Boolean!
name: String!
nativeName: String!
updatedAt: Date!
}
type LocalizationConfig {
locale: String!
autoUpdate: Boolean!
namespacing: Boolean!
namespaces: [String]!
isInstalled: Boolean
isRTL: Boolean
name: String
nativeName: String
updatedAt: Date
}
type Translation {
key: String!
value: String!
key: String
value: String
}
# ===============================================
# LOGGING
# ===============================================
extend type Query {
logging: LoggingQuery
}
extend type Mutation {
logging: LoggingMutation
}
extend type Subscription {
loggingLiveTrail: LoggerTrailLine
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type LoggingQuery {
loggers(
filter: String
orderBy: String
): [Logger] @auth(requires: ["manage:system"])
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type LoggingMutation {
updateLoggers(
loggers: [LoggerInput]
): DefaultResponse @auth(requires: ["manage:system"])
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type Logger {
isEnabled: Boolean!
key: String!
title: String!
description: String
logo: String
website: String
level: String
config: [KeyValuePair]
}
input LoggerInput {
isEnabled: Boolean!
key: String!
level: String!
config: [KeyValuePairInput]
}
type LoggerTrailLine {
level: String!
output: String!
timestamp: Date!
}
......@@ -3,31 +3,15 @@
# ===============================================
extend type Query {
mail: MailQuery
mailConfig: MailConfig
}
extend type Mutation {
mail: MailMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type MailQuery {
config: MailConfig @auth(requires: ["manage:system"])
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type MailMutation {
sendTest(
sendMailTest(
recipientEmail: String!
): DefaultResponse @auth(requires: ["manage:system"])
): DefaultResponse
updateConfig(
updateMailConfig(
senderName: String!
senderEmail: String!
host: String!
......@@ -40,7 +24,7 @@ type MailMutation {
dkimDomainName: String!
dkimKeySelector: String!
dkimPrivateKey: String!
): DefaultResponse @auth(requires: ["manage:system"])
): DefaultResponse
}
# -----------------------------------------------
......@@ -48,16 +32,16 @@ type MailMutation {
# -----------------------------------------------
type MailConfig {
senderName: String!
senderEmail: String!
host: String!
port: Int!
secure: Boolean!
verifySSL: Boolean!
user: String!
pass: String!
useDKIM: Boolean!
dkimDomainName: String!
dkimKeySelector: String!
dkimPrivateKey: String!
senderName: String
senderEmail: String
host: String
port: Int
secure: Boolean
verifySSL: Boolean
user: String
pass: String
useDKIM: Boolean
dkimDomainName: String
dkimKeySelector: String
dkimPrivateKey: String
}
......@@ -3,33 +3,21 @@
# ===============================================
extend type Query {
navigation: NavigationQuery
}
extend type Mutation {
navigation: NavigationMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type NavigationQuery {
tree: [NavigationTree]! @auth(requires: ["manage:navigation", "manage:system"])
config: NavigationConfig! @auth(requires: ["manage:navigation", "manage:system"])
navigationTree: [NavigationTree]
navigationConfig: NavigationConfig
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type NavigationMutation {
updateTree(
extend type Mutation {
updateNavigationTree(
tree: [NavigationTreeInput]!
): DefaultResponse @auth(requires: ["manage:navigation", "manage:system"])
updateConfig(
): DefaultResponse
updateNavigationConfig(
mode: NavigationMode!
): DefaultResponse @auth(requires: ["manage:navigation", "manage:system"])
): DefaultResponse
}
# -----------------------------------------------
......@@ -37,8 +25,8 @@ type NavigationMutation {
# -----------------------------------------------
type NavigationTree {
locale: String!
items: [NavigationItem]!
locale: String
items: [NavigationItem]
}
input NavigationTreeInput {
......@@ -47,8 +35,8 @@ input NavigationTreeInput {
}
type NavigationItem {
id: String!
kind: String!
id: String
kind: String
label: String
icon: String
targetType: String
......@@ -69,7 +57,7 @@ input NavigationItemInput {
}
type NavigationConfig {
mode: NavigationMode!
mode: NavigationMode
}
enum NavigationMode {
......
......@@ -3,36 +3,24 @@
# ===============================================
extend type Query {
pages: PageQuery
}
extend type Mutation {
pages: PageMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type PageQuery {
history(
pageHistoryById(
id: Int!
offsetPage: Int
offsetSize: Int
): PageHistoryResult @auth(requires: ["manage:system", "read:history"])
): PageHistoryResult
version(
pageVersionById(
pageId: Int!
versionId: Int!
): PageVersion @auth(requires: ["manage:system", "read:history"])
): PageVersion
search(
searchPages(
query: String!
path: String
locale: String
): PageSearchResponse! @auth(requires: ["manage:system", "read:pages"])
): PageSearchResponse!
list(
pages(
limit: Int
orderBy: PageOrderBy
orderByDirection: PageOrderByDirection
......@@ -40,46 +28,42 @@ type PageQuery {
locale: String
creatorId: Int
authorId: Int
): [PageListItem!]! @auth(requires: ["manage:system", "read:pages"])
): [PageListItem!]!
single(
pageById(
id: Int!
): Page @auth(requires: ["read:pages", "manage:system"])
): Page
tags: [PageTag]! @auth(requires: ["manage:system", "read:pages"])
tags: [PageTag]!
searchTags(
query: String!
): [String]! @auth(requires: ["manage:system", "read:pages"])
): [String]!
tree(
pageTree(
path: String
parent: Int
mode: PageTreeMode!
locale: String!
includeAncestors: Boolean
): [PageTreeItem] @auth(requires: ["manage:system", "read:pages"])
): [PageTreeItem]
links(
pageLinks(
locale: String!
): [PageLinkItem] @auth(requires: ["manage:system", "read:pages"])
): [PageLinkItem]
checkConflicts(
id: Int!
checkoutDate: Date!
): Boolean! @auth(requires: ["write:pages", "manage:pages", "manage:system"])
): Boolean!
conflictLatest(
checkConflictsLatest(
id: Int!
): PageConflictLatest! @auth(requires: ["write:pages", "manage:pages", "manage:system"])
): PageConflictLatest!
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type PageMutation {
create(
extend type Mutation {
createPage(
content: String!
description: String!
editor: String!
......@@ -93,9 +77,9 @@ type PageMutation {
scriptJs: String
tags: [String]!
title: String!
): PageResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"])
): PageResponse
update(
updatePage(
id: Int!
content: String
description: String
......@@ -110,54 +94,54 @@ type PageMutation {
scriptJs: String
tags: [String]
title: String
): PageResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"])
): PageResponse
convert(
convertPage(
id: Int!
editor: String!
): DefaultResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"])
): DefaultResponse
move(
renamePage(
id: Int!
destinationPath: String!
destinationLocale: String!
): DefaultResponse @auth(requires: ["manage:pages", "manage:system"])
): DefaultResponse
delete(
deletePage(
id: Int!
): DefaultResponse @auth(requires: ["delete:pages", "manage:system"])
): DefaultResponse
deleteTag(
id: Int!
): DefaultResponse @auth(requires: ["manage:system"])
): DefaultResponse
updateTag(
id: Int!
tag: String!
title: String!
): DefaultResponse @auth(requires: ["manage:system"])
): DefaultResponse
flushCache: DefaultResponse @auth(requires: ["manage:system"])
flushCache: DefaultResponse
migrateToLocale(
sourceLocale: String!
targetLocale: String!
): PageMigrationResponse @auth(requires: ["manage:system"])
): PageMigrationResponse
rebuildTree: DefaultResponse @auth(requires: ["manage:system"])
rebuildPageTree: DefaultResponse
render(
renderPage(
id: Int!
): DefaultResponse @auth(requires: ["manage:system"])
): DefaultResponse
restore(
restorePage(
pageId: Int!
versionId: Int!
): DefaultResponse @auth(requires: ["write:pages", "manage:pages", "manage:system"])
): DefaultResponse
purgeHistory (
purgePagesHistory (
olderThan: String!
): DefaultResponse @auth(requires: ["manage:system"])
): DefaultResponse
}
# -----------------------------------------------
......@@ -165,152 +149,152 @@ type PageMutation {
# -----------------------------------------------
type PageResponse {
responseResult: ResponseStatus!
operation: Operation
page: Page
}
type PageMigrationResponse {
responseResult: ResponseStatus!
operation: Operation
count: Int
}
type Page {
id: Int!
path: String!
hash: String!
title: String!
description: String!
isPrivate: Boolean! @auth(requires: ["write:pages", "manage:system"])
isPublished: Boolean! @auth(requires: ["write:pages", "manage:system"])
privateNS: String @auth(requires: ["write:pages", "manage:system"])
publishStartDate: Date! @auth(requires: ["write:pages", "manage:system"])
publishEndDate: Date! @auth(requires: ["write:pages", "manage:system"])
tags: [PageTag]!
content: String! @auth(requires: ["read:source", "write:pages", "manage:system"])
id: Int
path: String
hash: String
title: String
description: String
isPrivate: Boolean
isPublished: Boolean
privateNS: String
publishStartDate: Date
publishEndDate: Date
tags: [PageTag]
content: String
render: String
toc: String
contentType: String!
createdAt: Date!
updatedAt: Date!
editor: String! @auth(requires: ["write:pages", "manage:system"])
locale: String!
contentType: String
createdAt: Date
updatedAt: Date
editor: String
locale: String
scriptCss: String
scriptJs: String
authorId: Int! @auth(requires: ["write:pages", "manage:system"])
authorName: String! @auth(requires: ["write:pages", "manage:system"])
authorEmail: String! @auth(requires: ["write:pages", "manage:system"])
creatorId: Int! @auth(requires: ["write:pages", "manage:system"])
creatorName: String! @auth(requires: ["write:pages", "manage:system"])
creatorEmail: String! @auth(requires: ["write:pages", "manage:system"])
authorId: Int
authorName: String
authorEmail: String
creatorId: Int
creatorName: String
creatorEmail: String
}
type PageTag {
id: Int!
tag: String!
id: Int
tag: String
title: String
createdAt: Date!
updatedAt: Date!
createdAt: Date
updatedAt: Date
}
type PageHistory {
versionId: Int!
versionDate: Date!
authorId: Int!
authorName: String!
actionType: String!
versionId: Int
versionDate: Date
authorId: Int
authorName: String
actionType: String
valueBefore: String
valueAfter: String
}
type PageVersion {
action: String!
authorId: String!
authorName: String!
content: String!
contentType: String!
createdAt: Date!
versionDate: Date!
description: String!
editor: String!
isPrivate: Boolean!
isPublished: Boolean!
locale: String!
pageId: Int!
path: String!
publishEndDate: Date!
publishStartDate: Date!
tags: [String]!
title: String!
versionId: Int!
action: String
authorId: String
authorName: String
content: String
contentType: String
createdAt: Date
versionDate: Date
description: String
editor: String
isPrivate: Boolean
isPublished: Boolean
locale: String
pageId: Int
path: String
publishEndDate: Date
publishStartDate: Date
tags: [String]
title: String
versionId: Int
}
type PageHistoryResult {
trail: [PageHistory]
total: Int!
total: Int
}
type PageSearchResponse {
results: [PageSearchResult]!
suggestions: [String]!
totalHits: Int!
results: [PageSearchResult]
suggestions: [String]
totalHits: Int
}
type PageSearchResult {
id: String!
title: String!
description: String!
path: String!
locale: String!
id: String
title: String
description: String
path: String
locale: String
}
type PageListItem {
id: Int!
path: String!
locale: String!
id: Int
path: String
locale: String
title: String
description: String
contentType: String!
isPublished: Boolean!
isPrivate: Boolean!
contentType: String
isPublished: Boolean
isPrivate: Boolean
privateNS: String
createdAt: Date!
updatedAt: Date!
createdAt: Date
updatedAt: Date
tags: [String]
}
type PageTreeItem {
id: Int!
path: String!
depth: Int!
title: String!
isPrivate: Boolean!
isFolder: Boolean!
id: Int
path: String
depth: Int
title: String
isPrivate: Boolean
isFolder: Boolean
privateNS: String
parent: Int
pageId: Int
locale: String!
locale: String
}
type PageLinkItem {
id: Int!
path: String!
title: String!
links: [String]!
id: Int
path: String
title: String
links: [String]
}
type PageConflictLatest {
id: Int!
authorId: String!
authorName: String!
content: String!
createdAt: Date!
description: String!
isPublished: Boolean!
locale: String!
path: String!
id: Int
authorId: String
authorName: String
content: String
createdAt: Date
description: String
isPublished: Boolean
locale: String
path: String
tags: [String]
title: String!
updatedAt: Date!
title: String
updatedAt: Date
}
enum PageOrderBy {
......
......@@ -3,32 +3,16 @@
# ===============================================
extend type Query {
rendering: RenderingQuery
}
extend type Mutation {
rendering: RenderingMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type RenderingQuery {
renderers(
filter: String
orderBy: String
): [Renderer] @auth(requires: ["manage:system"])
): [Renderer]
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type RenderingMutation {
extend type Mutation {
updateRenderers(
renderers: [RendererInput]
): DefaultResponse @auth(requires: ["manage:system"])
renderers: [RendererInput]!
): DefaultResponse
}
# -----------------------------------------------
......@@ -36,9 +20,9 @@ type RenderingMutation {
# -----------------------------------------------
type Renderer {
isEnabled: Boolean!
key: String!
title: String!
isEnabled: Boolean
key: String
title: String
description: String
icon: String
dependsOn: String
......
......@@ -2,54 +2,10 @@
# SEARCH
# ===============================================
extend type Query {
search: SearchQuery
}
extend type Mutation {
search: SearchMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type SearchQuery {
searchEngines(
filter: String
orderBy: String
): [SearchEngine] @auth(requires: ["manage:system"])
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type SearchMutation {
updateSearchEngines(
engines: [SearchEngineInput]
): DefaultResponse @auth(requires: ["manage:system"])
rebuildIndex: DefaultResponse @auth(requires: ["manage:system"])
rebuildSearchIndex: DefaultResponse
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type SearchEngine {
isEnabled: Boolean!
key: String!
title: String!
description: String
logo: String
website: String
isAvailable: Boolean
config: [KeyValuePair]
}
input SearchEngineInput {
isEnabled: Boolean!
key: String!
config: [KeyValuePairInput]
}
......@@ -13,9 +13,6 @@ extend type Query {
hostname: String!
exact: Boolean!
): Site @auth(requires: ["manage:system"])
# Legacy
site: SiteQuery
}
extend type Mutation {
......@@ -42,59 +39,6 @@ extend type Mutation {
deleteSite (
id: UUID!
): DefaultResponse @auth(requires: ["manage:system"])
# Legacy
site: SiteMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type SiteQuery {
config: SiteConfig @auth(requires: ["manage:system"])
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type SiteMutation {
updateConfig(
host: String
title: String
description: String
robots: [String]
analyticsService: String
analyticsId: String
company: String
contentLicense: String
logoUrl: String
authAutoLogin: Boolean
authEnforce2FA: Boolean
authHideLocal: Boolean
authLoginBgUrl: String
authJwtAudience: String
authJwtExpiration: String
authJwtRenewablePeriod: String
featurePageRatings: Boolean
featurePageComments: Boolean
featurePersonalWikis: Boolean
securityOpenRedirect: Boolean
securityIframe: Boolean
securityReferrerPolicy: Boolean
securityTrustProxy: Boolean
securitySRI: Boolean
securityHSTS: Boolean
securityHSTSDuration: Int
securityCSP: Boolean
securityCSPDirectives: String
uploadMaxFileSize: Int
uploadMaxFiles: Int
uploadScanSVG: Boolean
uploadForceDownload: Boolean
): DefaultResponse @auth(requires: ["manage:system"])
}
# -----------------------------------------------
......@@ -174,7 +118,7 @@ enum SitePageRatingModes {
}
type SiteCreateResponse {
status: ResponseStatus
operation: Operation
site: Site
}
......@@ -227,40 +171,3 @@ input SiteThemeInput {
showSharingMenu: Boolean
showPrintBtn: Boolean
}
# LEGACY
type SiteConfig {
host: String
title: String
description: String
robots: [String]
analyticsService: String
analyticsId: String
company: String
contentLicense: String
logoUrl: String
authAutoLogin: Boolean
authEnforce2FA: Boolean
authHideLocal: Boolean
authLoginBgUrl: String
authJwtAudience: String
authJwtExpiration: String
authJwtRenewablePeriod: String
featurePageRatings: Boolean
featurePageComments: Boolean
featurePersonalWikis: Boolean
securityOpenRedirect: Boolean
securityIframe: Boolean
securityReferrerPolicy: Boolean
securityTrustProxy: Boolean
securitySRI: Boolean
securityHSTS: Boolean
securityHSTSDuration: Int
securityCSP: Boolean
securityCSPDirectives: String
uploadMaxFileSize: Int
uploadMaxFiles: Int
uploadScanSVG: Boolean
uploadForceDownload: Boolean
}
......@@ -3,35 +3,30 @@
# ===============================================
extend type Query {
storage: StorageQuery
storageTargets(
siteId: UUID!
): [StorageTarget]
}
extend type Mutation {
storage: StorageMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
updateStorageTargets(
siteId: UUID!
targets: [StorageTargetInput]!
): DefaultResponse
type StorageQuery {
targets: [StorageTarget] @auth(requires: ["manage:system"])
status: [StorageStatus] @auth(requires: ["manage:system"])
}
setupStorageTarget(
targetId: UUID!
state: JSON!
): StorageTargetSetupResponse
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
destroyStorageTargetSetup(
targetId: UUID!
): DefaultResponse
type StorageMutation {
updateTargets(
targets: [StorageTargetInput]!
): DefaultResponse @auth(requires: ["manage:system"])
executeAction(
targetKey: String!
executeStorageAction(
targetId: UUID!
handler: String!
): DefaultResponse @auth(requires: ["manage:system"])
): DefaultResponse
}
# -----------------------------------------------
......@@ -39,40 +34,46 @@ type StorageMutation {
# -----------------------------------------------
type StorageTarget {
isAvailable: Boolean!
isEnabled: Boolean!
key: String!
title: String!
id: UUID
isEnabled: Boolean
module: String
title: String
description: String
logo: String
icon: String
banner: String
vendor: String
website: String
supportedModes: [String]
mode: String
hasSchedule: Boolean!
syncInterval: String
syncIntervalDefault: String
config: [KeyValuePair]
actions: [StorageTargetAction]
contentTypes: JSON
assetDelivery: JSON
versioning: JSON
sync: JSON
status: JSON
setup: JSON
config: JSON
actions: JSON
}
input StorageTargetInput {
isEnabled: Boolean!
key: String!
mode: String!
syncInterval: String
config: [KeyValuePairInput]
type StorageTargetSetupResponse {
operation: Operation
state: JSON
}
type StorageStatus {
key: String!
title: String!
status: String!
message: String!
lastAttempt: String!
input StorageTargetInput {
id: UUID!
module: String!
isEnabled: Boolean
contentTypes: [String!]
largeThreshold: String
assetDeliveryFileStreaming: Boolean
assetDeliveryDirectAccess: Boolean
syncMode: StorageTargetSyncMode
syncInterval: String
useVersioning: Boolean
config: JSON
}
type StorageTargetAction {
handler: String!
label: String!
hint: String!
enum StorageTargetSyncMode {
PULL
PUSH
SYNC
}
......@@ -3,50 +3,41 @@
# ===============================================
extend type Query {
system: SystemQuery
systemExtensions: [SystemExtension]
systemFlags: [SystemFlag]
systemInfo: SystemInfo
systemSecurity: SystemSecurity
}
extend type Mutation {
system: SystemMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type SystemQuery {
flags: [SystemFlag] @auth(requires: ["manage:system"])
info: SystemInfo
extensions: [SystemExtension]! @auth(requires: ["manage:system"])
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type SystemMutation {
updateFlags(
updateSystemFlags(
flags: [SystemFlagInput]!
): DefaultResponse @auth(requires: ["manage:system"])
resetTelemetryClientId: DefaultResponse @auth(requires: ["manage:system"])
setTelemetry(
enabled: Boolean!
): DefaultResponse @auth(requires: ["manage:system"])
performUpgrade: DefaultResponse @auth(requires: ["manage:system"])
importUsersFromV1(
mongoDbConnString: String!
groupMode: SystemImportUsersGroupMode!
): SystemImportUsersResponse @auth(requires: ["manage:system"])
setHTTPSRedirection(
enabled: Boolean!
): DefaultResponse @auth(requires: ["manage:system"])
renewHTTPSCertificate: DefaultResponse @auth(requires: ["manage:system"])
): DefaultResponse
updateSystemSecurity(
authJwtAudience: String
authJwtExpiration: String
authJwtRenewablePeriod: String
corsConfig: String
corsMode: SystemSecurityCorsMode
cspDirectives: String
disallowFloc: Boolean
disallowIframe: Boolean
disallowOpenRedirect: Boolean
enforceCsp: Boolean
enforceHsts: Boolean
enforceSameOriginReferrerPolicy: Boolean
forceAssetDownload: Boolean
hstsDuration: Int
trustProxy: Boolean
uploadMaxFiles: Int
uploadMaxFileSize: Int
uploadScanSVG: Boolean
): DefaultResponse
installExtension(
key: String!
): DefaultResponse
}
# -----------------------------------------------
......@@ -54,8 +45,8 @@ type SystemMutation {
# -----------------------------------------------
type SystemFlag {
key: String!
value: Boolean!
key: String
value: Boolean
}
input SystemFlagInput {
......@@ -64,35 +55,35 @@ input SystemFlagInput {
}
type SystemInfo {
configFile: String @auth(requires: ["manage:system"])
cpuCores: Int @auth(requires: ["manage:system"])
currentVersion: String @auth(requires: ["manage:system"])
dbHost: String @auth(requires: ["manage:system"])
dbType: String @auth(requires: ["manage:system"])
dbVersion: String @auth(requires: ["manage:system"])
groupsTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:groups", "write:groups", "manage:users", "write:users"])
hostname: String @auth(requires: ["manage:system"])
httpPort: Int @auth(requires: ["manage:system"])
httpRedirection: Boolean @auth(requires: ["manage:system"])
httpsPort: Int @auth(requires: ["manage:system"])
latestVersion: String @auth(requires: ["manage:system"])
latestVersionReleaseDate: Date @auth(requires: ["manage:system"])
nodeVersion: String @auth(requires: ["manage:system"])
operatingSystem: String @auth(requires: ["manage:system"])
pagesTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:pages", "delete:pages"])
platform: String @auth(requires: ["manage:system"])
ramTotal: String @auth(requires: ["manage:system"])
sslDomain: String @auth(requires: ["manage:system"])
sslExpirationDate: Date @auth(requires: ["manage:system"])
sslProvider: String @auth(requires: ["manage:system"])
sslStatus: String @auth(requires: ["manage:system"])
sslSubscriberEmail: String @auth(requires: ["manage:system"])
tagsTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:pages", "delete:pages"])
telemetry: Boolean @auth(requires: ["manage:system"])
telemetryClientId: String @auth(requires: ["manage:system"])
upgradeCapable: Boolean @auth(requires: ["manage:system"])
usersTotal: Int @auth(requires: ["manage:system", "manage:navigation", "manage:groups", "write:groups", "manage:users", "write:users"])
workingDirectory: String @auth(requires: ["manage:system"])
configFile: String
cpuCores: Int
currentVersion: String
dbHost: String
dbType: String
dbVersion: String
groupsTotal: Int
hostname: String
httpPort: Int
httpRedirection: Boolean
httpsPort: Int
latestVersion: String
latestVersionReleaseDate: Date
nodeVersion: String
operatingSystem: String
pagesTotal: Int
platform: String
ramTotal: String
sslDomain: String
sslExpirationDate: Date
sslProvider: String
sslStatus: String
sslSubscriberEmail: String
tagsTotal: Int
telemetry: Boolean
telemetryClientId: String
upgradeCapable: Boolean
usersTotal: Int
workingDirectory: String
}
enum SystemImportUsersGroupMode {
......@@ -102,7 +93,7 @@ enum SystemImportUsersGroupMode {
}
type SystemImportUsersResponse {
responseResult: ResponseStatus
operation: Operation
usersCount: Int
groupsCount: Int
failed: [SystemImportUsersResponseFailed]
......@@ -115,9 +106,38 @@ type SystemImportUsersResponseFailed {
}
type SystemExtension {
key: String!
title: String!
description: String!
isInstalled: Boolean!
isCompatible: Boolean!
key: String
title: String
description: String
isInstalled: Boolean
isInstallable: Boolean
isCompatible: Boolean
}
type SystemSecurity {
authJwtAudience: String
authJwtExpiration: String
authJwtRenewablePeriod: String
corsConfig: String
corsMode: SystemSecurityCorsMode
cspDirectives: String
disallowFloc: Boolean
disallowIframe: Boolean
disallowOpenRedirect: Boolean
enforceCsp: Boolean
enforceHsts: Boolean
enforceSameOriginReferrerPolicy: Boolean
forceAssetDownload: Boolean
hstsDuration: Int
trustProxy: Boolean
uploadMaxFiles: Int
uploadMaxFileSize: Int
uploadScanSVG: Boolean
}
enum SystemSecurityCorsMode {
OFF
REFLECT
HOSTNAMES
REGEX
}
# ===============================================
# THEMES
# ===============================================
extend type Query {
theming: ThemingQuery
}
extend type Mutation {
theming: ThemingMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type ThemingQuery {
themes: [ThemingTheme] @auth(requires: ["manage:theme", "manage:system"])
config: ThemingConfig @auth(requires: ["manage:theme", "manage:system"])
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type ThemingMutation {
setConfig(
theme: String!
iconset: String!
darkMode: Boolean!
injectCSS: String
injectHead: String
injectBody: String
): DefaultResponse @auth(requires: ["manage:theme", "manage:system"])
}
# -----------------------------------------------
# TYPES
# -----------------------------------------------
type ThemingConfig {
theme: String!
iconset: String!
darkMode: Boolean!
injectCSS: String
injectHead: String
injectBody: String
}
type ThemingTheme {
key: String
title: String
author: String
}
......@@ -3,90 +3,65 @@
# ===============================================
extend type Query {
users: UserQuery
}
extend type Mutation {
users: UserMutation
}
# -----------------------------------------------
# QUERIES
# -----------------------------------------------
type UserQuery {
list(
users (
page: Int
pageSize: Int
orderBy: UserOrderBy
orderByDirection: OrderByDirection
# Filter by name / email
filter: String
orderBy: String
): [UserMinimal] @auth(requires: ["write:users", "manage:users", "manage:system"])
search(
query: String!
): [UserMinimal] @auth(requires: ["write:groups", "manage:groups", "write:users", "manage:users", "manage:system"])
): [UserMinimal]
single(
id: Int!
): User @auth(requires: ["manage:users", "manage:system"])
userById(
id: UUID!
): User
profile: UserProfile
lastLogins: [UserLastLogin] @auth(requires: ["write:groups", "manage:groups", "write:users", "manage:users", "manage:system"])
lastLogins: [UserLastLogin]
}
# -----------------------------------------------
# MUTATIONS
# -----------------------------------------------
type UserMutation {
create(
extend type Mutation {
createUser(
email: String!
name: String!
passwordRaw: String
providerKey: String!
groups: [Int]!
mustChangePassword: Boolean
sendWelcomeEmail: Boolean
): UserResponse @auth(requires: ["write:users", "manage:users", "manage:system"])
update(
id: Int!
email: String
name: String
newPassword: String
groups: [Int]
location: String
jobTitle: String
timezone: String
dateFormat: String
appearance: String
): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
delete(
id: Int!
replaceId: Int!
): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
password: String!
groups: [UUID]!
mustChangePassword: Boolean!
sendWelcomeEmail: Boolean!
): UserResponse
updateUser(
id: UUID!
patch: UserUpdateInput!
): DefaultResponse
verify(
id: Int!
): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
deleteUser(
id: UUID!
replaceId: UUID!
): DefaultResponse
activate(
id: Int!
): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
verifyUser(
id: UUID!
): DefaultResponse
deactivate(
id: Int!
): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
activateUser(
id: UUID!
): DefaultResponse
enableTFA(
id: Int!
): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
deactivateUser(
id: UUID!
): DefaultResponse
disableTFA(
id: Int!
): DefaultResponse @auth(requires: ["manage:users", "manage:system"])
enableUserTFA(
id: UUID!
): DefaultResponse
resetPassword(
disableUserTFA(
id: UUID!
): DefaultResponse
resetUserPassword(
id: Int!
): DefaultResponse
......@@ -110,71 +85,83 @@ type UserMutation {
# -----------------------------------------------
type UserResponse {
responseResult: ResponseStatus!
operation: Operation
user: User
}
type UserLastLogin {
id: Int!
name: String!
lastLoginAt: Date!
id: UUID
name: String
lastLoginAt: Date
}
type UserMinimal {
id: Int!
name: String!
email: String!
providerKey: String!
isSystem: Boolean!
isActive: Boolean!
createdAt: Date!
id: UUID
name: String
email: String
isSystem: Boolean
isActive: Boolean
createdAt: Date
lastLoginAt: Date
}
type User {
id: Int!
name: String!
email: String!
providerKey: String!
providerName: String
providerId: String
providerIs2FACapable: Boolean
isSystem: Boolean!
isActive: Boolean!
isVerified: Boolean!
location: String!
jobTitle: String!
timezone: String!
dateFormat: String!
appearance: String!
createdAt: Date!
updatedAt: Date!
id: UUID
name: String
email: String
auth: JSON
isSystem: Boolean
isActive: Boolean
isVerified: Boolean
meta: JSON
prefs: JSON
createdAt: Date
updatedAt: Date
lastLoginAt: Date
tfaIsActive: Boolean!
groups: [Group]!
groups: [Group]
}
type UserProfile {
id: Int!
name: String!
email: String!
id: Int
name: String
email: String
providerKey: String
providerName: String
isSystem: Boolean!
isVerified: Boolean!
location: String!
jobTitle: String!
timezone: String!
dateFormat: String!
appearance: String!
createdAt: Date!
updatedAt: Date!
isSystem: Boolean
isVerified: Boolean
location: String
jobTitle: String
timezone: String
dateFormat: String
appearance: String
createdAt: Date
updatedAt: Date
lastLoginAt: Date
groups: [String]!
pagesTotal: Int!
groups: [String]
pagesTotal: Int
}
type UserTokenResponse {
responseResult: ResponseStatus!
operation: Operation
jwt: String
}
enum UserOrderBy {
id
email
name
createdAt
updatedAt
lastLoginAt
}
input UserUpdateInput {
email: String
name: String
newPassword: String
groups: [UUID!]
isActive: Boolean
isVerified: Boolean
meta: JSON
prefs: JSON
}
<template lang="pug">
q-dialog(ref='dialog', @hide='onDialogHide')
q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 350px; max-width: 450px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-shutdown.svg', left, size='sm')
span {{value ? $t(`admin.sites.activate`) : $t(`admin.sites.deactivate`)}}
span {{modelValue ? t(`admin.sites.activate`) : t(`admin.sites.deactivate`)}}
q-card-section
.text-body2
i18n-t(:keypath='value ? `admin.sites.activateConfirm` : `admin.sites.deactivateConfirm`')
i18n-t(:keypath='modelValue ? `admin.sites.activateConfirm` : `admin.sites.deactivateConfirm`')
template(v-slot:siteTitle)
strong {{site.title}}
strong {{props.site.title}}
q-card-actions.card-actions
q-space
q-btn.acrylic-btn(
flat
:label='$t(`common.actions.cancel`)'
:label='t(`common.actions.cancel`)'
color='grey'
padding='xs md'
@click='hide'
@click='onDialogCancel'
)
q-btn(
unelevated
:label='value ? $t(`common.actions.activate`) : $t(`common.actions.deactivate`)'
:color='value ? `positive` : `negative`'
:label='modelValue ? t(`common.actions.activate`) : t(`common.actions.deactivate`)'
:color='modelValue ? `positive` : `negative`'
padding='xs md'
@click='confirm'
:loading='state.isLoading'
)
</template>
<script>
<script setup>
import gql from 'graphql-tag'
import cloneDeep from 'lodash/cloneDeep'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
import { reactive, ref } from 'vue'
export default {
props: {
site: {
type: Object
},
value: {
type: Boolean,
default: false
}
},
emits: ['ok', 'hide'],
data () {
return {
}
import { useAdminStore } from '../stores/admin'
// PROPS
const props = defineProps({
site: {
type: Object,
required: true
},
methods: {
show () {
this.$refs.dialog.show()
},
hide () {
this.$refs.dialog.hide()
},
onDialogHide () {
this.$emit('hide')
},
async confirm () {
try {
const siteId = this.site.id
const resp = await this.$apollo.mutate({
mutation: gql`
mutation updateSite (
$id: UUID!
$newState: Boolean
) {
updateSite(
id: $id
patch: {
isEnabled: $newState
}
) {
status {
succeeded
message
}
}
modelValue: {
type: Boolean,
default: false
}
})
// EMITS
defineEmits([
...useDialogPluginComponent.emits
])
// QUASAR
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
const $q = useQuasar()
// STORES
const adminStore = useAdminStore()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
isLoading: false
})
// METHODS
async function confirm () {
state.isLoading = true
try {
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation updateSite (
$id: UUID!
$newState: Boolean
) {
updateSite(
id: $id
patch: {
isEnabled: $newState
}
`,
variables: {
id: siteId,
newState: this.value
}
})
if (resp?.data?.updateSite?.status?.succeeded) {
this.$q.notify({
type: 'positive',
message: this.$t('admin.sites.updateSuccess')
})
this.$store.set('admin/sites', this.$store.get('admin/sites').map(s => {
if (s.id === siteId) {
const ns = cloneDeep(s)
ns.isEnabled = this.value
return ns
} else {
return s
) {
operation {
succeeded
message
}
}))
this.$emit('ok')
this.hide()
} else {
throw new Error(resp?.data?.updateSite?.status?.message || 'An unexpected error occured.')
}
}
} catch (err) {
this.$q.notify({
type: 'negative',
message: err.message
})
`,
variables: {
id: props.site.id,
newState: props.modelValue
}
})
if (resp?.data?.updateSite?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: this.$t('admin.sites.updateSuccess')
})
adminStore.$patch({
sites: adminStore.sites.map(s => {
if (s.id === props.site.id) {
const ns = cloneDeep(s)
ns.isEnabled = props.modelValue
return ns
} else {
return s
}
})
})
onDialogOK()
} else {
throw new Error(resp?.data?.updateSite?.operation?.message || 'An unexpected error occured.')
}
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
}
state.isLoading = false
}
</script>
......@@ -66,6 +66,8 @@ import { reactive, ref } from 'vue'
import { useAdminStore } from '../stores/admin'
// EMITS
defineEmits([
...useDialogPluginComponent.emits
])
......@@ -114,7 +116,7 @@ async function create () {
hostname: $hostname
title: $title
) {
status {
operation {
succeeded
message
}
......@@ -126,7 +128,7 @@ async function create () {
title: state.siteName
}
})
if (resp?.data?.createSite?.status?.succeeded) {
if (resp?.data?.createSite?.operation?.succeeded) {
$q.notify({
type: 'positive',
message: t('admin.sites.createSuccess')
......@@ -134,7 +136,7 @@ async function create () {
await adminStore.fetchSites()
onDialogOK()
} else {
throw new Error(resp?.data?.createSite?.status?.message || 'An unexpected error occured.')
throw new Error(resp?.data?.createSite?.operation?.message || 'An unexpected error occured.')
}
} catch (err) {
$q.notify({
......
<template lang="pug">
q-dialog(ref='dialog', @hide='onDialogHide')
q-dialog(ref='dialogRef', @hide='onDialogHide')
q-card(style='min-width: 350px; max-width: 450px;')
q-card-section.card-header
q-icon(name='img:/_assets/icons/fluent-delete-bin.svg', left, size='sm')
span {{$t(`admin.sites.delete`)}}
span {{t(`admin.sites.delete`)}}
q-card-section
.text-body2
i18n-t(keypath='admin.sites.deleteConfirm')
template(v-slot:siteTitle)
strong {{site.title}}
strong {{props.site.title}}
.text-body2.q-mt-md
strong.text-negative {{$t(`admin.sites.deleteConfirmWarn`)}}
strong.text-negative {{t(`admin.sites.deleteConfirmWarn`)}}
q-card-actions.card-actions
q-space
q-btn.acrylic-btn(
flat
:label='$t(`common.actions.cancel`)'
:label='t(`common.actions.cancel`)'
color='grey'
padding='xs md'
@click='hide'
@click='onDialogCancel'
)
q-btn(
unelevated
:label='$t(`common.actions.delete`)'
:label='t(`common.actions.delete`)'
color='negative'
padding='xs md'
@click='confirm'
:loading='state.isLoading'
)
</template>
<script>
<script setup>
import gql from 'graphql-tag'
import { useI18n } from 'vue-i18n'
import { useDialogPluginComponent, useQuasar } from 'quasar'
import { reactive } from 'vue'
export default {
props: {
site: {
type: Object
}
},
emits: ['ok', 'hide'],
data () {
return {
}
},
methods: {
show () {
this.$refs.dialog.show()
},
hide () {
this.$refs.dialog.hide()
},
onDialogHide () {
this.$emit('hide')
},
async confirm () {
try {
const siteId = this.site.id
const resp = await this.$apollo.mutate({
mutation: gql`
mutation deleteSite ($id: UUID!) {
deleteSite(id: $id) {
status {
succeeded
message
}
}
import { useAdminStore } from '../stores/admin'
// PROPS
const props = defineProps({
site: {
type: Object,
required: true
}
})
// EMITS
defineEmits([
...useDialogPluginComponent.emits
])
// QUASAR
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } = useDialogPluginComponent()
const $q = useQuasar()
// STORES
const adminStore = useAdminStore()
// I18N
const { t } = useI18n()
// DATA
const state = reactive({
isLoading: false
})
// METHODS
async function confirm () {
state.isLoading = true
try {
const resp = await APOLLO_CLIENT.mutate({
mutation: gql`
mutation deleteSite ($id: UUID!) {
deleteSite(id: $id) {
status {
succeeded
message
}
`,
variables: {
id: siteId
}
})
if (resp?.data?.deleteSite?.status?.succeeded) {
this.$q.notify({
type: 'positive',
message: this.$t('admin.sites.deleteSuccess')
})
this.$store.set('admin/sites', this.$store.get('admin/sites').filter(s => s.id !== siteId))
this.$emit('ok')
this.hide()
} else {
throw new Error(resp?.data?.deleteSite?.status?.message || 'An unexpected error occured.')
}
} catch (err) {
this.$q.notify({
type: 'negative',
message: err.message
})
`,
variables: {
id: props.site.id
}
})
if (resp?.data?.deleteSite?.status?.succeeded) {
$q.notify({
type: 'positive',
message: t('admin.sites.deleteSuccess')
})
adminStore.$patch({
sites: adminStore.sites.filter(s => s.id !== props.site.id)
})
onDialogOK()
} else {
throw new Error(resp?.data?.deleteSite?.status?.message || 'An unexpected error occured.')
}
} catch (err) {
$q.notify({
type: 'negative',
message: err.message
})
}
state.isLoading = false
}
</script>
......@@ -133,10 +133,6 @@ useMeta({
title: t('admin.sites.title')
})
// DATA
const loading = ref(false)
// METHODS
async function refresh () {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment