const graphHelper = require('../../helpers/graph') const _ = require('lodash') const CleanCSS = require('clean-css') const path = require('path') /* global WIKI */ module.exports = { Query: { async sites () { const sites = await WIKI.models.sites.query() return sites.map(s => ({ ...s.config, id: s.id, hostname: s.hostname, isEnabled: s.isEnabled })) }, async siteById (obj, args) { const site = await WIKI.models.sites.query().findById(args.id) return site ? { ...site.config, id: site.id, hostname: site.hostname, isEnabled: site.isEnabled } : null }, async siteByHostname (obj, args) { let site = await WIKI.models.sites.query().where({ hostname: args.hostname }).first() if (!site && !args.exact) { site = await WIKI.models.sites.query().where({ hostname: '*' }).first() } return site ? { ...site.config, id: site.id, hostname: site.hostname, isEnabled: site.isEnabled } : null }, // LEGACY async site() { return {} } }, Mutation: { /** * CREATE SITE */ async createSite (obj, args) { try { // -> Validate inputs if (!args.hostname || args.hostname.length < 1 || !/^(\\*)|([a-z0-9\-.:]+)$/.test(args.hostname)) { throw WIKI.ERROR(new Error('Invalid Site Hostname'), 'SiteCreateInvalidHostname') } if (!args.title || args.title.length < 1 || !/^[^<>"]+$/.test(args.title)) { throw WIKI.ERROR(new Error('Invalid Site Title'), 'SiteCreateInvalidTitle') } // -> Check for duplicate catch-all if (args.hostname === '*') { const site = await WIKI.models.sites.query().where({ hostname: args.hostname }).first() if (site) { throw WIKI.ERROR(new Error('A site with a catch-all hostname already exists! Cannot have 2 catch-all hostnames.'), 'SiteCreateDuplicateCatchAll') } } // -> Create site const newSite = await WIKI.models.sites.createSite(args.hostname, { title: args.title }) return { status: graphHelper.generateSuccess('Site created successfully'), site: newSite } } catch (err) { return graphHelper.generateError(err) } }, /** * UPDATE SITE */ async updateSite (obj, args) { try { // -> Load site const site = await WIKI.models.sites.query().findById(args.id) if (!site) { throw WIKI.ERROR(new Error('Invalid Site ID'), 'SiteInvalidId') } // -> Check for bad input if (_.has(args.patch, 'hostname') && _.trim(args.patch.hostname).length < 1) { throw WIKI.ERROR(new Error('Hostname is invalid.'), 'SiteInvalidHostname') } // -> Check for duplicate catch-all if (args.patch.hostname === '*' && site.hostname !== '*') { const dupSite = await WIKI.models.sites.query().where({ hostname: '*' }).first() if (dupSite) { throw WIKI.ERROR(new Error(`Site ${dupSite.config.title} with a catch-all hostname already exists! Cannot have 2 catch-all hostnames.`), 'SiteUpdateDuplicateCatchAll') } } // -> Format Code if (args.patch?.theme?.injectCSS) { args.patch.theme.injectCSS = new CleanCSS({ inline: false }).minify(args.patch.theme.injectCSS).styles } // -> Update site await WIKI.models.sites.updateSite(args.id, { hostname: args.patch.hostname ?? site.hostname, isEnabled: args.patch.isEnabled ?? site.isEnabled, config: _.defaultsDeep(_.omit(args.patch, ['hostname', 'isEnabled']), site.config) }) return { status: graphHelper.generateSuccess('Site updated successfully') } } catch (err) { WIKI.logger.warn(err) return graphHelper.generateError(err) } }, /** * DELETE SITE */ async deleteSite (obj, args) { try { // -> Ensure site isn't last one const sitesCount = await WIKI.models.sites.query().count('id').first() if (sitesCount?.count && _.toNumber(sitesCount?.count) <= 1) { throw WIKI.ERROR(new Error('Cannot delete the last site. At least 1 site must exists at all times.'), 'SiteDeleteLastSite') } // -> Delete site await WIKI.models.sites.deleteSite(args.id) return { status: graphHelper.generateSuccess('Site deleted successfully') } } catch (err) { WIKI.logger.warn(err) return graphHelper.generateError(err) } }, /** * UPLOAD LOGO */ async uploadSiteLogo (obj, args) { try { const { filename, mimetype, createReadStream } = await args.image WIKI.logger.info(`Processing site logo ${filename} of type ${mimetype}...`) if (!WIKI.extensions.ext.sharp.isInstalled) { throw new Error('This feature requires the Sharp extension but it is not installed.') } console.info(mimetype) const destFormat = mimetype.startsWith('image/svg') ? 'svg' : 'png' const destPath = path.resolve( process.cwd(), WIKI.config.dataPath, `assets/logo.${destFormat}` ) await WIKI.extensions.ext.sharp.resize({ format: destFormat, inputStream: createReadStream(), outputPath: destPath, width: 100 }) WIKI.logger.info('New site logo processed successfully.') return { status: graphHelper.generateSuccess('Site logo uploaded successfully') } } catch (err) { return graphHelper.generateError(err) } }, /** * UPLOAD FAVICON */ async uploadSiteFavicon (obj, args) { const { filename, mimetype, createReadStream } = await args.image console.info(filename, mimetype) 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) } } } }