site.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  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, context) {
  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('assets').insert({
  184. id: site.config.assets.logo,
  185. filename: `_logo.${destFormat}`,
  186. hash: '_logo',
  187. fileExt: `.${destFormat}`,
  188. isSystem: true,
  189. kind: 'image',
  190. mimeType: (destFormat === 'svg') ? 'image/svg' : 'image/png',
  191. fileSize: Math.ceil(imgBuffer.byteLength / 1024),
  192. data: imgBuffer,
  193. authorId: context.req.user.id,
  194. siteId: site.id
  195. }).onConflict('id').merge()
  196. WIKI.logger.info('New site logo processed successfully.')
  197. return {
  198. operation: graphHelper.generateSuccess('Site logo uploaded successfully')
  199. }
  200. } catch (err) {
  201. WIKI.logger.warn(err)
  202. return graphHelper.generateError(err)
  203. }
  204. },
  205. /**
  206. * UPLOAD FAVICON
  207. */
  208. async uploadSiteFavicon (obj, args, context) {
  209. try {
  210. const { filename, mimetype, createReadStream } = await args.image
  211. WIKI.logger.info(`Processing site favicon ${filename} of type ${mimetype}...`)
  212. if (!WIKI.extensions.ext.sharp.isInstalled) {
  213. throw new Error('This feature requires the Sharp extension but it is not installed.')
  214. }
  215. if (!['.svg', '.png', '.jpg', '.webp', '.gif'].some(s => filename.endsWith(s))) {
  216. throw new Error('Invalid File Extension. Must be svg, png, jpg, webp or gif.')
  217. }
  218. const destFormat = mimetype.startsWith('image/svg') ? 'svg' : 'png'
  219. const destFolder = path.resolve(
  220. process.cwd(),
  221. WIKI.config.dataPath,
  222. `assets`
  223. )
  224. const destPath = path.join(destFolder, `favicon-${args.id}.${destFormat}`)
  225. await fs.ensureDir(destFolder)
  226. // -> Resize
  227. await WIKI.extensions.ext.sharp.resize({
  228. format: destFormat,
  229. inputStream: createReadStream(),
  230. outputPath: destPath,
  231. width: 64,
  232. height: 64
  233. })
  234. // -> Save favicon meta to DB
  235. const site = await WIKI.db.sites.query().findById(args.id)
  236. if (!site.config.assets.favicon) {
  237. site.config.assets.favicon = uuid()
  238. }
  239. site.config.assets.faviconExt = destFormat
  240. await WIKI.db.sites.query().findById(args.id).patch({ config: site.config })
  241. await WIKI.db.sites.reloadCache()
  242. // -> Save image data to DB
  243. const imgBuffer = await fs.readFile(destPath)
  244. await WIKI.db.knex('assets').insert({
  245. id: site.config.assets.favicon,
  246. filename: `_favicon.${destFormat}`,
  247. hash: '_favicon',
  248. ext: `.${destFormat}`,
  249. isSystem: true,
  250. kind: 'image',
  251. mime: (destFormat === 'svg') ? 'image/svg' : 'image/png',
  252. fileSize: Math.ceil(imgBuffer.byteLength / 1024),
  253. data: imgBuffer,
  254. authorId: context.req.user.id,
  255. siteId: site.id
  256. }).onConflict('id').merge()
  257. WIKI.logger.info('New site favicon processed successfully.')
  258. return {
  259. operation: graphHelper.generateSuccess('Site favicon uploaded successfully')
  260. }
  261. } catch (err) {
  262. WIKI.logger.warn(err)
  263. return graphHelper.generateError(err)
  264. }
  265. },
  266. /**
  267. * UPLOAD LOGIN BG
  268. */
  269. async uploadSiteLoginBg (obj, args, context) {
  270. try {
  271. const { filename, mimetype, createReadStream } = await args.image
  272. WIKI.logger.info(`Processing site login bg ${filename} of type ${mimetype}...`)
  273. if (!WIKI.extensions.ext.sharp.isInstalled) {
  274. throw new Error('This feature requires the Sharp extension but it is not installed.')
  275. }
  276. if (!['.png', '.jpg', '.webp'].some(s => filename.endsWith(s))) {
  277. throw new Error('Invalid File Extension. Must be png, jpg or webp.')
  278. }
  279. const destFolder = path.resolve(
  280. process.cwd(),
  281. WIKI.config.dataPath,
  282. `assets`
  283. )
  284. const destPath = path.join(destFolder, `loginbg-${args.id}.jpg`)
  285. await fs.ensureDir(destFolder)
  286. // -> Resize
  287. await WIKI.extensions.ext.sharp.resize({
  288. format: 'jpg',
  289. inputStream: createReadStream(),
  290. outputPath: destPath,
  291. width: 1920
  292. })
  293. // -> Save login bg meta to DB
  294. const site = await WIKI.db.sites.query().findById(args.id)
  295. if (!site.config.assets.loginBg) {
  296. site.config.assets.loginBg = uuid()
  297. await WIKI.db.sites.query().findById(args.id).patch({ config: site.config })
  298. await WIKI.db.sites.reloadCache()
  299. }
  300. // -> Save image data to DB
  301. const imgBuffer = await fs.readFile(destPath)
  302. await WIKI.db.knex('assets').insert({
  303. id: site.config.assets.loginBg,
  304. filename: '_loginbg.jpg',
  305. hash: '_loginbg',
  306. ext: '.jpg',
  307. isSystem: true,
  308. kind: 'image',
  309. mime: 'image/jpg',
  310. fileSize: Math.ceil(imgBuffer.byteLength / 1024),
  311. data: imgBuffer,
  312. authorId: context.req.user.id,
  313. siteId: site.id
  314. }).onConflict('id').merge()
  315. WIKI.logger.info('New site login bg processed successfully.')
  316. return {
  317. operation: graphHelper.generateSuccess('Site login bg uploaded successfully')
  318. }
  319. } catch (err) {
  320. WIKI.logger.warn(err)
  321. return graphHelper.generateError(err)
  322. }
  323. }
  324. }
  325. }