site.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. const graphHelper = require('../../helpers/graph')
  2. const _ = require('lodash')
  3. const CleanCSS = require('clean-css')
  4. const path = require('path')
  5. const fs = require('fs-extra')
  6. const { v4: uuid } = require('uuid')
  7. module.exports = {
  8. Query: {
  9. async sites () {
  10. const sites = await WIKI.db.sites.query().orderBy('hostname')
  11. return sites.map(s => ({
  12. ...s.config,
  13. id: s.id,
  14. hostname: s.hostname,
  15. isEnabled: s.isEnabled,
  16. pageExtensions: s.config.pageExtensions.join(', ')
  17. }))
  18. },
  19. async siteById (obj, args) {
  20. const site = await WIKI.db.sites.query().findById(args.id)
  21. return site ? {
  22. ...site.config,
  23. id: site.id,
  24. hostname: site.hostname,
  25. isEnabled: site.isEnabled,
  26. pageExtensions: site.config.pageExtensions.join(', ')
  27. } : null
  28. },
  29. async siteByHostname (obj, args) {
  30. let site = await WIKI.db.sites.query().where({
  31. hostname: args.hostname
  32. }).first()
  33. if (!site && !args.exact) {
  34. site = await WIKI.db.sites.query().where({
  35. hostname: '*'
  36. }).first()
  37. }
  38. return site ? {
  39. ...site.config,
  40. id: site.id,
  41. hostname: site.hostname,
  42. isEnabled: site.isEnabled,
  43. pageExtensions: site.config.pageExtensions.join(', ')
  44. } : null
  45. }
  46. },
  47. Mutation: {
  48. /**
  49. * CREATE SITE
  50. */
  51. async createSite (obj, args) {
  52. try {
  53. // -> Validate inputs
  54. if (!args.hostname || args.hostname.length < 1 || !/^(\\*)|([a-z0-9\-.:]+)$/.test(args.hostname)) {
  55. throw new WIKI.Error.Custom('SiteCreateInvalidHostname', 'Invalid Site Hostname')
  56. }
  57. if (!args.title || args.title.length < 1 || !/^[^<>"]+$/.test(args.title)) {
  58. throw new WIKI.Error.Custom('SiteCreateInvalidTitle', 'Invalid Site Title')
  59. }
  60. // -> Check for duplicate catch-all
  61. if (args.hostname === '*') {
  62. const site = await WIKI.db.sites.query().where({
  63. hostname: args.hostname
  64. }).first()
  65. if (site) {
  66. throw new WIKI.Error.Custom('SiteCreateDuplicateCatchAll', 'A site with a catch-all hostname already exists! Cannot have 2 catch-all hostnames.')
  67. }
  68. }
  69. // -> Create site
  70. const newSite = await WIKI.db.sites.createSite(args.hostname, {
  71. title: args.title
  72. })
  73. return {
  74. operation: graphHelper.generateSuccess('Site created successfully'),
  75. site: newSite
  76. }
  77. } catch (err) {
  78. WIKI.logger.warn(err)
  79. return graphHelper.generateError(err)
  80. }
  81. },
  82. /**
  83. * UPDATE SITE
  84. */
  85. async updateSite (obj, args) {
  86. try {
  87. // -> Load site
  88. const site = await WIKI.db.sites.query().findById(args.id)
  89. if (!site) {
  90. throw new WIKI.Error.Custom('SiteInvalidId', 'Invalid Site ID')
  91. }
  92. // -> Check for bad input
  93. if (_.has(args.patch, 'hostname') && _.trim(args.patch.hostname).length < 1) {
  94. throw new WIKI.Error.Custom('SiteInvalidHostname', 'Hostname is invalid.')
  95. }
  96. // -> Check for duplicate catch-all
  97. if (args.patch.hostname === '*' && site.hostname !== '*') {
  98. const dupSite = await WIKI.db.sites.query().where({ hostname: '*' }).first()
  99. if (dupSite) {
  100. throw new WIKI.Error.Custom('SiteUpdateDuplicateCatchAll', `Site ${dupSite.config.title} with a catch-all hostname already exists! Cannot have 2 catch-all hostnames.`)
  101. }
  102. }
  103. // -> Format Code
  104. if (args.patch?.theme?.injectCSS) {
  105. args.patch.theme.injectCSS = new CleanCSS({ inline: false }).minify(args.patch.theme.injectCSS).styles
  106. }
  107. // -> Format Page Extensions
  108. if (args.patch?.pageExtensions) {
  109. args.patch.pageExtensions = args.patch.pageExtensions.split(',').map(ext => ext.trim().toLowerCase()).filter(ext => ext.length > 0)
  110. }
  111. // -> Update site
  112. await WIKI.db.sites.updateSite(args.id, {
  113. hostname: args.patch.hostname ?? site.hostname,
  114. isEnabled: args.patch.isEnabled ?? site.isEnabled,
  115. config: _.defaultsDeep(_.omit(args.patch, ['hostname', 'isEnabled']), site.config)
  116. })
  117. return {
  118. operation: graphHelper.generateSuccess('Site updated successfully')
  119. }
  120. } catch (err) {
  121. WIKI.logger.warn(err)
  122. return graphHelper.generateError(err)
  123. }
  124. },
  125. /**
  126. * DELETE SITE
  127. */
  128. async deleteSite (obj, args) {
  129. try {
  130. // -> Ensure site isn't last one
  131. const sitesCount = await WIKI.db.sites.query().count('id').first()
  132. if (sitesCount?.count && _.toNumber(sitesCount?.count) <= 1) {
  133. throw new WIKI.Error.Custom('SiteDeleteLastSite', 'Cannot delete the last site. At least 1 site must exists at all times.')
  134. }
  135. // -> Delete site
  136. await WIKI.db.sites.deleteSite(args.id)
  137. return {
  138. operation: graphHelper.generateSuccess('Site deleted successfully')
  139. }
  140. } catch (err) {
  141. WIKI.logger.warn(err)
  142. return graphHelper.generateError(err)
  143. }
  144. },
  145. /**
  146. * UPLOAD LOGO
  147. */
  148. async uploadSiteLogo (obj, args) {
  149. try {
  150. const { filename, mimetype, createReadStream } = await args.image
  151. WIKI.logger.info(`Processing site logo ${filename} of type ${mimetype}...`)
  152. if (!WIKI.extensions.ext.sharp.isInstalled) {
  153. throw new Error('This feature requires the Sharp extension but it is not installed.')
  154. }
  155. if (!['.svg', '.png', '.jpg', 'webp', '.gif'].some(s => filename.endsWith(s))) {
  156. throw new Error('Invalid File Extension. Must be svg, png, jpg, webp or gif.')
  157. }
  158. const destFormat = mimetype.startsWith('image/svg') ? 'svg' : 'png'
  159. const destFolder = path.resolve(
  160. process.cwd(),
  161. WIKI.config.dataPath,
  162. `assets`
  163. )
  164. const destPath = path.join(destFolder, `logo-${args.id}.${destFormat}`)
  165. await fs.ensureDir(destFolder)
  166. // -> Resize
  167. await WIKI.extensions.ext.sharp.resize({
  168. format: destFormat,
  169. inputStream: createReadStream(),
  170. outputPath: destPath,
  171. height: 72
  172. })
  173. // -> Save logo meta to DB
  174. const site = await WIKI.db.sites.query().findById(args.id)
  175. if (!site.config.assets.logo) {
  176. site.config.assets.logo = uuid()
  177. }
  178. site.config.assets.logoExt = destFormat
  179. await WIKI.db.sites.query().findById(args.id).patch({ config: site.config })
  180. await WIKI.db.sites.reloadCache()
  181. // -> Save image data to DB
  182. const imgBuffer = await fs.readFile(destPath)
  183. await WIKI.db.knex('assetData').insert({
  184. id: site.config.assets.logo,
  185. data: imgBuffer
  186. }).onConflict('id').merge()
  187. WIKI.logger.info('New site logo processed successfully.')
  188. return {
  189. operation: graphHelper.generateSuccess('Site logo uploaded successfully')
  190. }
  191. } catch (err) {
  192. WIKI.logger.warn(err)
  193. return graphHelper.generateError(err)
  194. }
  195. },
  196. /**
  197. * UPLOAD FAVICON
  198. */
  199. async uploadSiteFavicon (obj, args) {
  200. try {
  201. const { filename, mimetype, createReadStream } = await args.image
  202. WIKI.logger.info(`Processing site favicon ${filename} of type ${mimetype}...`)
  203. if (!WIKI.extensions.ext.sharp.isInstalled) {
  204. throw new Error('This feature requires the Sharp extension but it is not installed.')
  205. }
  206. if (!['.svg', '.png', '.jpg', '.webp', '.gif'].some(s => filename.endsWith(s))) {
  207. throw new Error('Invalid File Extension. Must be svg, png, jpg, webp or gif.')
  208. }
  209. const destFormat = mimetype.startsWith('image/svg') ? 'svg' : 'png'
  210. const destFolder = path.resolve(
  211. process.cwd(),
  212. WIKI.config.dataPath,
  213. `assets`
  214. )
  215. const destPath = path.join(destFolder, `favicon-${args.id}.${destFormat}`)
  216. await fs.ensureDir(destFolder)
  217. // -> Resize
  218. await WIKI.extensions.ext.sharp.resize({
  219. format: destFormat,
  220. inputStream: createReadStream(),
  221. outputPath: destPath,
  222. width: 64,
  223. height: 64
  224. })
  225. // -> Save favicon meta to DB
  226. const site = await WIKI.db.sites.query().findById(args.id)
  227. if (!site.config.assets.favicon) {
  228. site.config.assets.favicon = uuid()
  229. }
  230. site.config.assets.faviconExt = destFormat
  231. await WIKI.db.sites.query().findById(args.id).patch({ config: site.config })
  232. await WIKI.db.sites.reloadCache()
  233. // -> Save image data to DB
  234. const imgBuffer = await fs.readFile(destPath)
  235. await WIKI.db.knex('assetData').insert({
  236. id: site.config.assets.favicon,
  237. data: imgBuffer
  238. }).onConflict('id').merge()
  239. WIKI.logger.info('New site favicon processed successfully.')
  240. return {
  241. operation: graphHelper.generateSuccess('Site favicon uploaded successfully')
  242. }
  243. } catch (err) {
  244. WIKI.logger.warn(err)
  245. return graphHelper.generateError(err)
  246. }
  247. },
  248. /**
  249. * UPLOAD LOGIN BG
  250. */
  251. async uploadSiteLoginBg (obj, args) {
  252. try {
  253. const { filename, mimetype, createReadStream } = await args.image
  254. WIKI.logger.info(`Processing site login bg ${filename} of type ${mimetype}...`)
  255. if (!WIKI.extensions.ext.sharp.isInstalled) {
  256. throw new Error('This feature requires the Sharp extension but it is not installed.')
  257. }
  258. if (!['.png', '.jpg', '.webp'].some(s => filename.endsWith(s))) {
  259. throw new Error('Invalid File Extension. Must be png, jpg or webp.')
  260. }
  261. const destFolder = path.resolve(
  262. process.cwd(),
  263. WIKI.config.dataPath,
  264. `assets`
  265. )
  266. const destPath = path.join(destFolder, `loginbg-${args.id}.jpg`)
  267. await fs.ensureDir(destFolder)
  268. // -> Resize
  269. await WIKI.extensions.ext.sharp.resize({
  270. format: 'jpg',
  271. inputStream: createReadStream(),
  272. outputPath: destPath,
  273. width: 1920
  274. })
  275. // -> Save login bg meta to DB
  276. const site = await WIKI.db.sites.query().findById(args.id)
  277. if (!site.config.assets.loginBg) {
  278. site.config.assets.loginBg = uuid()
  279. await WIKI.db.sites.query().findById(args.id).patch({ config: site.config })
  280. await WIKI.db.sites.reloadCache()
  281. }
  282. // -> Save image data to DB
  283. const imgBuffer = await fs.readFile(destPath)
  284. await WIKI.db.knex('assetData').insert({
  285. id: site.config.assets.loginBg,
  286. data: imgBuffer
  287. }).onConflict('id').merge()
  288. WIKI.logger.info('New site login bg processed successfully.')
  289. return {
  290. operation: graphHelper.generateSuccess('Site login bg uploaded successfully')
  291. }
  292. } catch (err) {
  293. WIKI.logger.warn(err)
  294. return graphHelper.generateError(err)
  295. }
  296. }
  297. }
  298. }