site.mjs 12 KB

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