site.js 11 KB

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