mirror of https://github.com/requarks/wiki
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
355 lines
13 KiB
355 lines
13 KiB
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
|
|
import _ from 'lodash-es'
|
|
import CleanCSS from 'clean-css'
|
|
import path from 'node:path'
|
|
import fs from 'fs-extra'
|
|
import { v4 as uuid } from 'uuid'
|
|
|
|
export default {
|
|
Query: {
|
|
async sites () {
|
|
const sites = await WIKI.db.sites.query().orderBy('hostname')
|
|
return sites.map(s => ({
|
|
...s.config,
|
|
id: s.id,
|
|
hostname: s.hostname,
|
|
isEnabled: s.isEnabled,
|
|
pageExtensions: s.config.pageExtensions.join(', ')
|
|
}))
|
|
},
|
|
async siteById (obj, args) {
|
|
const site = await WIKI.db.sites.query().findById(args.id)
|
|
return site ? {
|
|
...site.config,
|
|
id: site.id,
|
|
hostname: site.hostname,
|
|
isEnabled: site.isEnabled,
|
|
pageExtensions: site.config.pageExtensions.join(', ')
|
|
} : null
|
|
},
|
|
async siteByHostname (obj, args) {
|
|
let site = await WIKI.db.sites.query().where({
|
|
hostname: args.hostname
|
|
}).first()
|
|
if (!site && !args.exact) {
|
|
site = await WIKI.db.sites.query().where({
|
|
hostname: '*'
|
|
}).first()
|
|
}
|
|
return site ? {
|
|
...site.config,
|
|
id: site.id,
|
|
hostname: site.hostname,
|
|
isEnabled: site.isEnabled,
|
|
pageExtensions: site.config.pageExtensions.join(', ')
|
|
} : null
|
|
}
|
|
},
|
|
Mutation: {
|
|
/**
|
|
* CREATE SITE
|
|
*/
|
|
async createSite (obj, args, context) {
|
|
try {
|
|
if (!WIKI.auth.checkAccess(context.req.user, ['write:sites', 'manage:sites'])) {
|
|
throw new Error('ERR_FORBIDDEN')
|
|
}
|
|
|
|
// -> Validate inputs
|
|
if (!args.hostname || args.hostname.length < 1 || !/^(\\*)|([a-z0-9\-.:]+)$/.test(args.hostname)) {
|
|
throw new WIKI.Error.Custom('SiteCreateInvalidHostname', 'Invalid Site Hostname')
|
|
}
|
|
if (!args.title || args.title.length < 1 || !/^[^<>"]+$/.test(args.title)) {
|
|
throw new WIKI.Error.Custom('SiteCreateInvalidTitle', 'Invalid Site Title')
|
|
}
|
|
// -> Check for duplicate catch-all
|
|
if (args.hostname === '*') {
|
|
const site = await WIKI.db.sites.query().where({
|
|
hostname: args.hostname
|
|
}).first()
|
|
if (site) {
|
|
throw new WIKI.Error.Custom('SiteCreateDuplicateCatchAll', 'A site with a catch-all hostname already exists! Cannot have 2 catch-all hostnames.')
|
|
}
|
|
}
|
|
// -> Create site
|
|
const newSite = await WIKI.db.sites.createSite(args.hostname, {
|
|
title: args.title
|
|
})
|
|
return {
|
|
operation: generateSuccess('Site created successfully'),
|
|
site: newSite
|
|
}
|
|
} catch (err) {
|
|
WIKI.logger.warn(err)
|
|
return generateError(err)
|
|
}
|
|
},
|
|
/**
|
|
* UPDATE SITE
|
|
*/
|
|
async updateSite (obj, args, context) {
|
|
try {
|
|
if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
|
|
throw new Error('ERR_FORBIDDEN')
|
|
}
|
|
|
|
// -> Load site
|
|
const site = await WIKI.db.sites.query().findById(args.id)
|
|
if (!site) {
|
|
throw new WIKI.Error.Custom('SiteInvalidId', 'Invalid Site ID')
|
|
}
|
|
// -> Check for bad input
|
|
if (_.has(args.patch, 'hostname') && _.trim(args.patch.hostname).length < 1) {
|
|
throw new WIKI.Error.Custom('SiteInvalidHostname', 'Hostname is invalid.')
|
|
}
|
|
// -> Check for duplicate catch-all
|
|
if (args.patch.hostname === '*' && site.hostname !== '*') {
|
|
const dupSite = await WIKI.db.sites.query().where({ hostname: '*' }).first()
|
|
if (dupSite) {
|
|
throw new WIKI.Error.Custom('SiteUpdateDuplicateCatchAll', `Site ${dupSite.config.title} with a catch-all hostname already exists! Cannot have 2 catch-all hostnames.`)
|
|
}
|
|
}
|
|
// -> Format Code
|
|
if (args.patch?.theme?.injectCSS) {
|
|
args.patch.theme.injectCSS = new CleanCSS({ inline: false }).minify(args.patch.theme.injectCSS).styles
|
|
}
|
|
// -> Format Page Extensions
|
|
if (args.patch?.pageExtensions) {
|
|
args.patch.pageExtensions = args.patch.pageExtensions.split(',').map(ext => ext.trim().toLowerCase()).filter(ext => ext.length > 0)
|
|
}
|
|
// -> Update site
|
|
await WIKI.db.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 {
|
|
operation: generateSuccess('Site updated successfully')
|
|
}
|
|
} catch (err) {
|
|
WIKI.logger.warn(err)
|
|
return generateError(err)
|
|
}
|
|
},
|
|
/**
|
|
* DELETE SITE
|
|
*/
|
|
async deleteSite (obj, args, context) {
|
|
try {
|
|
if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
|
|
throw new Error('ERR_FORBIDDEN')
|
|
}
|
|
|
|
// -> Ensure site isn't last one
|
|
const sitesCount = await WIKI.db.sites.query().count('id').first()
|
|
if (sitesCount?.count && _.toNumber(sitesCount?.count) <= 1) {
|
|
throw new WIKI.Error.Custom('SiteDeleteLastSite', 'Cannot delete the last site. At least 1 site must exists at all times.')
|
|
}
|
|
// -> Delete site
|
|
await WIKI.db.sites.deleteSite(args.id)
|
|
return {
|
|
operation: generateSuccess('Site deleted successfully')
|
|
}
|
|
} catch (err) {
|
|
WIKI.logger.warn(err)
|
|
return generateError(err)
|
|
}
|
|
},
|
|
/**
|
|
* UPLOAD LOGO
|
|
*/
|
|
async uploadSiteLogo (obj, args, context) {
|
|
try {
|
|
if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
|
|
throw new Error('ERR_FORBIDDEN')
|
|
}
|
|
|
|
const { filename, mimetype, createReadStream } = await args.image
|
|
WIKI.logger.info(`Processing site logo ${filename} of type ${mimetype}...`)
|
|
if (!WIKI.extensions.ext.sharp.isInstalled) {
|
|
throw new Error('This feature requires the Sharp extension but it is not installed.')
|
|
}
|
|
if (!['.svg', '.png', '.jpg', 'webp', '.gif'].some(s => filename.endsWith(s))) {
|
|
throw new Error('Invalid File Extension. Must be svg, png, jpg, webp or gif.')
|
|
}
|
|
const destFormat = mimetype.startsWith('image/svg') ? 'svg' : 'png'
|
|
const destFolder = path.resolve(
|
|
process.cwd(),
|
|
WIKI.config.dataPath,
|
|
`assets`
|
|
)
|
|
const destPath = path.join(destFolder, `logo-${args.id}.${destFormat}`)
|
|
await fs.ensureDir(destFolder)
|
|
// -> Resize
|
|
await WIKI.extensions.ext.sharp.resize({
|
|
format: destFormat,
|
|
inputStream: createReadStream(),
|
|
outputPath: destPath,
|
|
height: 72
|
|
})
|
|
// -> Save logo meta to DB
|
|
const site = await WIKI.db.sites.query().findById(args.id)
|
|
if (!site.config.assets.logo) {
|
|
site.config.assets.logo = uuid()
|
|
}
|
|
site.config.assets.logoExt = destFormat
|
|
await WIKI.db.sites.query().findById(args.id).patch({ config: site.config })
|
|
await WIKI.db.sites.reloadCache()
|
|
// -> Save image data to DB
|
|
const imgBuffer = await fs.readFile(destPath)
|
|
await WIKI.db.knex('assets').insert({
|
|
id: site.config.assets.logo,
|
|
fileName: `_logo.${destFormat}`,
|
|
fileExt: `.${destFormat}`,
|
|
isSystem: true,
|
|
kind: 'image',
|
|
mimeType: (destFormat === 'svg') ? 'image/svg' : 'image/png',
|
|
fileSize: Math.ceil(imgBuffer.byteLength / 1024),
|
|
data: imgBuffer,
|
|
authorId: context.req.user.id,
|
|
siteId: site.id
|
|
}).onConflict('id').merge()
|
|
WIKI.logger.info('New site logo processed successfully.')
|
|
return {
|
|
operation: generateSuccess('Site logo uploaded successfully')
|
|
}
|
|
} catch (err) {
|
|
WIKI.logger.warn(err)
|
|
return generateError(err)
|
|
}
|
|
},
|
|
/**
|
|
* UPLOAD FAVICON
|
|
*/
|
|
async uploadSiteFavicon (obj, args, context) {
|
|
try {
|
|
if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
|
|
throw new Error('ERR_FORBIDDEN')
|
|
}
|
|
|
|
const { filename, mimetype, createReadStream } = await args.image
|
|
WIKI.logger.info(`Processing site favicon ${filename} of type ${mimetype}...`)
|
|
if (!WIKI.extensions.ext.sharp.isInstalled) {
|
|
throw new Error('This feature requires the Sharp extension but it is not installed.')
|
|
}
|
|
if (!['.svg', '.png', '.jpg', '.webp', '.gif'].some(s => filename.endsWith(s))) {
|
|
throw new Error('Invalid File Extension. Must be svg, png, jpg, webp or gif.')
|
|
}
|
|
const destFormat = mimetype.startsWith('image/svg') ? 'svg' : 'png'
|
|
const destFolder = path.resolve(
|
|
process.cwd(),
|
|
WIKI.config.dataPath,
|
|
`assets`
|
|
)
|
|
const destPath = path.join(destFolder, `favicon-${args.id}.${destFormat}`)
|
|
await fs.ensureDir(destFolder)
|
|
// -> Resize
|
|
await WIKI.extensions.ext.sharp.resize({
|
|
format: destFormat,
|
|
inputStream: createReadStream(),
|
|
outputPath: destPath,
|
|
width: 64,
|
|
height: 64
|
|
})
|
|
// -> Save favicon meta to DB
|
|
const site = await WIKI.db.sites.query().findById(args.id)
|
|
if (!site.config.assets.favicon) {
|
|
site.config.assets.favicon = uuid()
|
|
}
|
|
site.config.assets.faviconExt = destFormat
|
|
await WIKI.db.sites.query().findById(args.id).patch({ config: site.config })
|
|
await WIKI.db.sites.reloadCache()
|
|
// -> Save image data to DB
|
|
const imgBuffer = await fs.readFile(destPath)
|
|
await WIKI.db.knex('assets').insert({
|
|
id: site.config.assets.favicon,
|
|
fileName: `_favicon.${destFormat}`,
|
|
fileExt: `.${destFormat}`,
|
|
isSystem: true,
|
|
kind: 'image',
|
|
mimeType: (destFormat === 'svg') ? 'image/svg' : 'image/png',
|
|
fileSize: Math.ceil(imgBuffer.byteLength / 1024),
|
|
data: imgBuffer,
|
|
authorId: context.req.user.id,
|
|
siteId: site.id
|
|
}).onConflict('id').merge()
|
|
WIKI.logger.info('New site favicon processed successfully.')
|
|
return {
|
|
operation: generateSuccess('Site favicon uploaded successfully')
|
|
}
|
|
} catch (err) {
|
|
WIKI.logger.warn(err)
|
|
return generateError(err)
|
|
}
|
|
},
|
|
/**
|
|
* UPLOAD LOGIN BG
|
|
*/
|
|
async uploadSiteLoginBg (obj, args, context) {
|
|
try {
|
|
if (!WIKI.auth.checkAccess(context.req.user, ['manage:sites'])) {
|
|
throw new Error('ERR_FORBIDDEN')
|
|
}
|
|
|
|
const { filename, mimetype, createReadStream } = await args.image
|
|
WIKI.logger.info(`Processing site login bg ${filename} of type ${mimetype}...`)
|
|
if (!WIKI.extensions.ext.sharp.isInstalled) {
|
|
throw new Error('This feature requires the Sharp extension but it is not installed.')
|
|
}
|
|
if (!['.png', '.jpg', '.webp'].some(s => filename.endsWith(s))) {
|
|
throw new Error('Invalid File Extension. Must be png, jpg or webp.')
|
|
}
|
|
const destFolder = path.resolve(
|
|
process.cwd(),
|
|
WIKI.config.dataPath,
|
|
`assets`
|
|
)
|
|
const destPath = path.join(destFolder, `loginbg-${args.id}.jpg`)
|
|
await fs.ensureDir(destFolder)
|
|
// -> Resize
|
|
await WIKI.extensions.ext.sharp.resize({
|
|
format: 'jpg',
|
|
inputStream: createReadStream(),
|
|
outputPath: destPath,
|
|
width: 1920
|
|
})
|
|
// -> Save login bg meta to DB
|
|
const site = await WIKI.db.sites.query().findById(args.id)
|
|
if (!site.config.assets.loginBg) {
|
|
site.config.assets.loginBg = uuid()
|
|
await WIKI.db.sites.query().findById(args.id).patch({ config: site.config })
|
|
await WIKI.db.sites.reloadCache()
|
|
}
|
|
// -> Save image data to DB
|
|
const imgBuffer = await fs.readFile(destPath)
|
|
await WIKI.db.knex('assets').insert({
|
|
id: site.config.assets.loginBg,
|
|
filename: '_loginbg.jpg',
|
|
hash: '_loginbg',
|
|
ext: '.jpg',
|
|
isSystem: true,
|
|
kind: 'image',
|
|
mime: 'image/jpg',
|
|
fileSize: Math.ceil(imgBuffer.byteLength / 1024),
|
|
data: imgBuffer,
|
|
authorId: context.req.user.id,
|
|
siteId: site.id
|
|
}).onConflict('id').merge()
|
|
WIKI.logger.info('New site login bg processed successfully.')
|
|
return {
|
|
operation: generateSuccess('Site login bg uploaded successfully')
|
|
}
|
|
} catch (err) {
|
|
WIKI.logger.warn(err)
|
|
return generateError(err)
|
|
}
|
|
}
|
|
},
|
|
SiteLocales: {
|
|
async active (obj, args, context) {
|
|
return obj.active.map(l => WIKI.cache.get(`locale:${l}`))
|
|
}
|
|
}
|
|
}
|