浏览代码

refactor: convert to esm (wip)

NGPixel 2 年之前
父节点
当前提交
e9e93eff42
共有 97 个文件被更改,包括 2775 次插入2823 次删除
  1. 1 3
      server/app/regex.mjs
  2. 0 158
      server/controllers/auth.js
  3. 161 0
      server/controllers/auth.mjs
  4. 0 533
      server/controllers/common.js
  5. 534 0
      server/controllers/common.mjs
  6. 0 36
      server/controllers/ssl.js
  7. 39 0
      server/controllers/ssl.mjs
  8. 0 105
      server/controllers/upload.js
  9. 5 5
      server/controllers/ws.mjs
  10. 8 9
      server/core/asar.mjs
  11. 16 15
      server/core/auth.mjs
  12. 0 7
      server/core/cache.js
  13. 22 22
      server/core/config.mjs
  14. 25 26
      server/core/db.mjs
  15. 5 5
      server/core/extensions.mjs
  16. 21 13
      server/core/kernel.mjs
  17. 0 60
      server/core/logger.js
  18. 61 0
      server/core/logger.mjs
  19. 12 12
      server/core/mail.mjs
  20. 17 13
      server/core/scheduler.mjs
  21. 19 17
      server/core/servers.mjs
  22. 0 21
      server/core/system.js
  23. 21 0
      server/core/system.mjs
  24. 7 7
      server/db/migrations/3.0.0.mjs
  25. 0 27
      server/db/migrator-source.js
  26. 26 0
      server/db/migrator-source.mjs
  27. 0 54
      server/graph/index.js
  28. 61 0
      server/graph/index.mjs
  29. 10 10
      server/graph/resolvers/analytics.mjs
  30. 17 17
      server/graph/resolvers/asset.mjs
  31. 25 25
      server/graph/resolvers/authentication.mjs
  32. 11 11
      server/graph/resolvers/comment.mjs
  33. 10 10
      server/graph/resolvers/group.mjs
  34. 9 9
      server/graph/resolvers/hooks.mjs
  35. 7 7
      server/graph/resolvers/localization.mjs
  36. 7 7
      server/graph/resolvers/mail.mjs
  37. 6 6
      server/graph/resolvers/navigation.mjs
  38. 31 31
      server/graph/resolvers/page.mjs
  39. 5 5
      server/graph/resolvers/rendering.mjs
  40. 0 16
      server/graph/resolvers/search.js
  41. 16 0
      server/graph/resolvers/search.mjs
  42. 19 19
      server/graph/resolvers/site.mjs
  43. 12 12
      server/graph/resolvers/storage.mjs
  44. 21 19
      server/graph/resolvers/system.mjs
  45. 9 9
      server/graph/resolvers/tree.mjs
  46. 30 30
      server/graph/resolvers/user.mjs
  47. 3 3
      server/graph/scalars/date.mjs
  48. 2 2
      server/graph/scalars/json.mjs
  49. 2 2
      server/graph/scalars/uuid.mjs
  50. 4 2
      server/helpers/brute-knex.mjs
  51. 0 99
      server/helpers/common.js
  52. 102 0
      server/helpers/common.mjs
  53. 3 5
      server/helpers/config.mjs
  54. 2 2
      server/helpers/error.mjs
  55. 0 21
      server/helpers/graph.js
  56. 20 0
      server/helpers/graph.mjs
  57. 0 151
      server/helpers/page.js
  58. 155 0
      server/helpers/page.mjs
  59. 0 38
      server/helpers/security.js
  60. 38 0
      server/helpers/security.mjs
  61. 16 12
      server/index.mjs
  62. 0 20
      server/locales/README.md
  63. 0 46
      server/middlewares/security.js
  64. 0 19
      server/middlewares/seo.js
  65. 17 17
      server/models/analytics.mjs
  66. 5 5
      server/models/apiKeys.mjs
  67. 17 24
      server/models/assets.mjs
  68. 10 10
      server/models/authentication.mjs
  69. 19 19
      server/models/commentProviders.mjs
  70. 10 8
      server/models/comments.mjs
  71. 5 3
      server/models/groups.mjs
  72. 2 2
      server/models/hooks.mjs
  73. 45 0
      server/models/index.mjs
  74. 2 2
      server/models/locales.mjs
  75. 5 5
      server/models/navigation.mjs
  76. 16 11
      server/models/pageHistory.mjs
  77. 5 3
      server/models/pageLinks.mjs
  78. 42 37
      server/models/pages.mjs
  79. 24 24
      server/models/renderers.mjs
  80. 5 5
      server/models/settings.mjs
  81. 5 7
      server/models/sites.mjs
  82. 11 12
      server/models/storage.mjs
  83. 13 11
      server/models/tags.mjs
  84. 29 27
      server/models/tree.mjs
  85. 7 5
      server/models/userKeys.mjs
  86. 67 64
      server/models/users.mjs
  87. 383 371
      server/package-lock.json
  88. 71 74
      server/package.json
  89. 1 1
      server/tasks/simple/check-version.mjs
  90. 2 2
      server/tasks/simple/clean-job-history.mjs
  91. 1 1
      server/tasks/simple/update-locales.mjs
  92. 8 8
      server/tasks/workers/purge-uploads.mjs
  93. 9 9
      server/tasks/workers/render-page.mjs
  94. 75 34
      server/web.mjs
  95. 6 6
      server/worker.js
  96. 175 175
      ux/package-lock.json
  97. 30 28
      ux/package.json

+ 1 - 3
server/app/regex.js → server/app/regex.mjs

@@ -1,6 +1,4 @@
-'use strict'
-
-module.exports = {
+export default {
   arabic: '\u0600-\u06ff\u0750-\u077f\ufb50-\ufc3f\ufe70-\ufefc',
   cjk: '\u4E00-\u9FBF\u3040-\u309F\u30A0-\u30FFㄱ-ㅎ가-힣ㅏ-ㅣ',
   youtube: /(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|&v(?:i)?=))([^#&?]*).*/,

+ 0 - 158
server/controllers/auth.js

@@ -1,158 +0,0 @@
-/* global WIKI */
-
-const express = require('express')
-const ExpressBrute = require('express-brute')
-const BruteKnex = require('../helpers/brute-knex')
-const router = express.Router()
-const moment = require('moment')
-const _ = require('lodash')
-const path = require('path')
-
-const bruteforce = new ExpressBrute(new BruteKnex({
-  createTable: true,
-  knex: WIKI.db.knex
-}), {
-  freeRetries: 5,
-  minWait: 5 * 60 * 1000, // 5 minutes
-  maxWait: 60 * 60 * 1000, // 1 hour
-  failCallback: (req, res, next) => {
-    res.status(401).send('Too many failed attempts. Try again later.')
-  }
-})
-
-/**
- * Login form
- */
-router.get('/login', async (req, res, next) => {
-  // -> Bypass Login
-  if (WIKI.config.auth.autoLogin && !req.query.all) {
-    const stg = await WIKI.db.authentication.query().orderBy('order').first()
-    const stgInfo = _.find(WIKI.data.authentication, ['key', stg.strategyKey])
-    if (!stgInfo.useForm) {
-      return res.redirect(`/login/${stg.key}`)
-    }
-  }
-  // -> Show Login
-  res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
-})
-
-/**
- * Social Strategies Login
- */
-router.get('/login/:strategy', async (req, res, next) => {
-  try {
-    await WIKI.db.users.login({
-      strategy: req.params.strategy
-    }, { req, res })
-  } catch (err) {
-    next(err)
-  }
-})
-
-/**
- * Social Strategies Callback
- */
-router.all('/login/:strategy/callback', async (req, res, next) => {
-  if (req.method !== 'GET' && req.method !== 'POST') { return next() }
-
-  try {
-    const authResult = await WIKI.db.users.login({
-      strategy: req.params.strategy
-    }, { req, res })
-    res.cookie('jwt', authResult.jwt, { expires: moment().add(1, 'y').toDate() })
-
-    const loginRedirect = req.cookies['loginRedirect']
-    if (loginRedirect === '/' && authResult.redirect) {
-      res.clearCookie('loginRedirect')
-      res.redirect(authResult.redirect)
-    } else if (loginRedirect) {
-      res.clearCookie('loginRedirect')
-      res.redirect(loginRedirect)
-    } else if (authResult.redirect) {
-      res.redirect(authResult.redirect)
-    } else {
-      res.redirect('/')
-    }
-  } catch (err) {
-    next(err)
-  }
-})
-
-/**
- * Logout
- */
-router.get('/logout', async (req, res, next) => {
-  const redirURL = await WIKI.db.users.logout({ req, res })
-  req.logout((err) => {
-    if (err) { return next(err) }
-    res.clearCookie('jwt')
-    res.redirect(redirURL)
-  })
-})
-
-/**
- * Register form
- */
-router.get('/register', async (req, res, next) => {
-  _.set(res.locals, 'pageMeta.title', 'Register')
-  const localStrg = await WIKI.db.authentication.getStrategy('local')
-  if (localStrg.selfRegistration) {
-    res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
-  } else {
-    next(new WIKI.Error.AuthRegistrationDisabled())
-  }
-})
-
-/**
- * Verify
- */
-router.get('/verify/:token', bruteforce.prevent, async (req, res, next) => {
-  try {
-    const usr = await WIKI.db.userKeys.validateToken({ kind: 'verify', token: req.params.token })
-    await WIKI.db.users.query().patch({ isVerified: true }).where('id', usr.id)
-    req.brute.reset()
-    if (WIKI.config.auth.enforce2FA) {
-      res.redirect('/login')
-    } else {
-      const result = await WIKI.db.users.refreshToken(usr)
-      res.cookie('jwt', result.token, { expires: moment().add(1, 'years').toDate() })
-      res.redirect('/')
-    }
-  } catch (err) {
-    next(err)
-  }
-})
-
-/**
- * Reset Password
- */
-router.get('/login-reset/:token', bruteforce.prevent, async (req, res, next) => {
-  try {
-    const usr = await WIKI.db.userKeys.validateToken({ kind: 'resetPwd', token: req.params.token })
-    if (!usr) {
-      throw new Error('Invalid Token')
-    }
-    req.brute.reset()
-
-    const changePwdContinuationToken = await WIKI.db.userKeys.generateToken({
-      userId: usr.id,
-      kind: 'changePwd'
-    })
-    const bgUrl = !_.isEmpty(WIKI.config.auth.loginBgUrl) ? WIKI.config.auth.loginBgUrl : '/_assets/img/splash/1.jpg'
-    res.render('login', { bgUrl, hideLocal: WIKI.config.auth.hideLocal, changePwdContinuationToken })
-  } catch (err) {
-    next(err)
-  }
-})
-
-/**
- * JWT Public Endpoints
- */
-router.get('/.well-known/jwk.json', function (req, res, next) {
-  res.json(WIKI.config.certs.jwk)
-})
-router.get('/.well-known/jwk.pem', function (req, res, next) {
-  res.send(WIKI.config.certs.public)
-})
-
-module.exports = router

+ 161 - 0
server/controllers/auth.mjs

@@ -0,0 +1,161 @@
+/* global WIKI */
+
+import express from 'express'
+import ExpressBrute from 'express-brute'
+import BruteKnex from '../helpers/brute-knex.mjs'
+import { find, isEmpty, set } from 'lodash-es'
+import path from 'node:path'
+import { DateTime } from 'luxon'
+
+export default function () {
+  const router = express.Router()
+
+  const bruteforce = new ExpressBrute(new BruteKnex({
+    createTable: true,
+    knex: WIKI.db.knex
+  }), {
+    freeRetries: 5,
+    minWait: 5 * 60 * 1000, // 5 minutes
+    maxWait: 60 * 60 * 1000, // 1 hour
+    failCallback: (req, res, next) => {
+      res.status(401).send('Too many failed attempts. Try again later.')
+    }
+  })
+
+  /**
+   * Login form
+   */
+  router.get('/login', async (req, res, next) => {
+    // -> Bypass Login
+    if (WIKI.config.auth.autoLogin && !req.query.all) {
+      const stg = await WIKI.db.authentication.query().orderBy('order').first()
+      const stgInfo = find(WIKI.data.authentication, ['key', stg.strategyKey])
+      if (!stgInfo.useForm) {
+        return res.redirect(`/login/${stg.key}`)
+      }
+    }
+    // -> Show Login
+    res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
+  })
+
+  /**
+   * Social Strategies Login
+   */
+  router.get('/login/:strategy', async (req, res, next) => {
+    try {
+      await WIKI.db.users.login({
+        strategy: req.params.strategy
+      }, { req, res })
+    } catch (err) {
+      next(err)
+    }
+  })
+
+  /**
+   * Social Strategies Callback
+   */
+  router.all('/login/:strategy/callback', async (req, res, next) => {
+    if (req.method !== 'GET' && req.method !== 'POST') { return next() }
+
+    try {
+      const authResult = await WIKI.db.users.login({
+        strategy: req.params.strategy
+      }, { req, res })
+      res.cookie('jwt', authResult.jwt, { expires: DateTime.now().plus({ years: 1 }).toJSDate() })
+
+      const loginRedirect = req.cookies['loginRedirect']
+      if (loginRedirect === '/' && authResult.redirect) {
+        res.clearCookie('loginRedirect')
+        res.redirect(authResult.redirect)
+      } else if (loginRedirect) {
+        res.clearCookie('loginRedirect')
+        res.redirect(loginRedirect)
+      } else if (authResult.redirect) {
+        res.redirect(authResult.redirect)
+      } else {
+        res.redirect('/')
+      }
+    } catch (err) {
+      next(err)
+    }
+  })
+
+  /**
+   * Logout
+   */
+  router.get('/logout', async (req, res, next) => {
+    const redirURL = await WIKI.db.users.logout({ req, res })
+    req.logout((err) => {
+      if (err) { return next(err) }
+      res.clearCookie('jwt')
+      res.redirect(redirURL)
+    })
+  })
+
+  /**
+   * Register form
+   */
+  router.get('/register', async (req, res, next) => {
+    set(res.locals, 'pageMeta.title', 'Register')
+    const localStrg = await WIKI.db.authentication.getStrategy('local')
+    if (localStrg.selfRegistration) {
+      res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
+    } else {
+      next(new WIKI.Error.AuthRegistrationDisabled())
+    }
+  })
+
+  /**
+   * Verify
+   */
+  router.get('/verify/:token', bruteforce.prevent, async (req, res, next) => {
+    try {
+      const usr = await WIKI.db.userKeys.validateToken({ kind: 'verify', token: req.params.token })
+      await WIKI.db.users.query().patch({ isVerified: true }).where('id', usr.id)
+      req.brute.reset()
+      if (WIKI.config.auth.enforce2FA) {
+        res.redirect('/login')
+      } else {
+        const result = await WIKI.db.users.refreshToken(usr)
+        res.cookie('jwt', result.token, { expires: DateTime.now().plus({ years: 1 }).toJSDate() })
+        res.redirect('/')
+      }
+    } catch (err) {
+      next(err)
+    }
+  })
+
+  /**
+   * Reset Password
+   */
+  router.get('/login-reset/:token', bruteforce.prevent, async (req, res, next) => {
+    try {
+      const usr = await WIKI.db.userKeys.validateToken({ kind: 'resetPwd', token: req.params.token })
+      if (!usr) {
+        throw new Error('Invalid Token')
+      }
+      req.brute.reset()
+
+      const changePwdContinuationToken = await WIKI.db.userKeys.generateToken({
+        userId: usr.id,
+        kind: 'changePwd'
+      })
+      const bgUrl = !isEmpty(WIKI.config.auth.loginBgUrl) ? WIKI.config.auth.loginBgUrl : '/_assets/img/splash/1.jpg'
+      res.render('login', { bgUrl, hideLocal: WIKI.config.auth.hideLocal, changePwdContinuationToken })
+    } catch (err) {
+      next(err)
+    }
+  })
+
+  /**
+   * JWT Public Endpoints
+   */
+  router.get('/.well-known/jwk.json', function (req, res, next) {
+    res.json(WIKI.config.certs.jwk)
+  })
+  router.get('/.well-known/jwk.pem', function (req, res, next) {
+    res.send(WIKI.config.certs.public)
+  })
+
+  return router
+}

+ 0 - 533
server/controllers/common.js

@@ -1,533 +0,0 @@
-const express = require('express')
-const router = express.Router()
-const pageHelper = require('../helpers/page')
-const _ = require('lodash')
-const CleanCSS = require('clean-css')
-const moment = require('moment')
-const path = require('path')
-const siteAssetsPath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'assets')
-
-/**
- * Robots.txt
- */
-router.get('/robots.txt', (req, res, next) => {
-  res.type('text/plain')
-  if (_.includes(WIKI.config.seo.robots, 'noindex')) {
-    res.send('User-agent: *\nDisallow: /')
-  } else {
-    res.status(200).end()
-  }
-})
-
-/**
- * Health Endpoint
- */
-router.get('/healthz', (req, res, next) => {
-  if (WIKI.db.knex.client.pool.numFree() < 1 && WIKI.db.knex.client.pool.numUsed() < 1) {
-    res.status(503).json({ ok: false }).end()
-  } else {
-    res.status(200).json({ ok: true }).end()
-  }
-})
-
-/**
- * Site Asset
- */
-router.get('/_site/:siteId?/:resource', async (req, res, next) => {
-  const site = req.params.siteId ? WIKI.sites[req.params.siteId] : await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
-  if (!site) {
-    return res.status(404).send('Site Not Found')
-  }
-  switch (req.params.resource) {
-    case 'logo': {
-      if (site.config.assets.logo) {
-        // TODO: Fetch from db if not in disk cache
-        res.sendFile(path.join(siteAssetsPath, `logo-${site.id}.${site.config.assets.logoExt}`))
-      } else {
-        res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/logo-wikijs.svg'))
-      }
-      break
-    }
-    case 'favicon': {
-      if (site.config.assets.favicon) {
-        // TODO: Fetch from db if not in disk cache
-        res.sendFile(path.join(siteAssetsPath, `favicon-${site.id}.${site.config.assets.faviconExt}`))
-      } else {
-        res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/logo-wikijs.svg'))
-      }
-      break
-    }
-    case 'loginbg': {
-      if (site.config.assets.loginBg) {
-        // TODO: Fetch from db if not in disk cache
-        res.sendFile(path.join(siteAssetsPath, `loginbg-${site.id}.jpg`))
-      } else {
-        res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/bg/login.jpg'))
-      }
-      break
-    }
-    default: {
-      return res.status(404).send('Invalid Site Resource')
-    }
-  }
-})
-
-/**
- * Asset Thumbnails / Download
- */
-router.get('/_thumb/:id.webp', async (req, res, next) => {
-  const thumb = await WIKI.db.assets.getThumbnail({
-    id: req.params.id
-  })
-
-  if (thumb) {
-    // TODO: Check permissions
-
-    switch (thumb.previewState) {
-      case 'pending': {
-        res.redirect('/_assets/illustrations/fileman-pending.svg')
-        break
-      }
-      case 'ready': {
-        res.set('Content-Type', 'image/webp')
-        res.send(thumb.preview)
-        break
-      }
-      case 'failed': {
-        res.redirect('/_assets/illustrations/fileman-failed.svg')
-        break
-      }
-      default: {
-        return res.status(500).send('Invalid Thumbnail Preview State')
-      }
-    }
-  } else {
-    return res.sendStatus(404)
-  }
-})
-
-// router.get(['/_admin', '/_admin/*'], (req, res, next) => {
-//   if (!WIKI.auth.checkAccess(req.user, [
-//     'manage:system',
-//     'write:users',
-//     'manage:users',
-//     'write:groups',
-//     'manage:groups',
-//     'manage:navigation',
-//     'manage:theme',
-//     'manage:api'
-//   ])) {
-//     _.set(res.locals, 'pageMeta.title', 'Unauthorized')
-//     return res.status(403).render('unauthorized', { action: 'view' })
-//   }
-
-//   _.set(res.locals, 'pageMeta.title', 'Admin')
-//   res.render('admin')
-
-// })
-
-// /**
-//  * Download Page / Version
-//  */
-// router.get(['/d', '/d/*'], async (req, res, next) => {
-//   const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
-
-//   const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0
-
-//   const page = await WIKI.db.pages.getPageFromDb({
-//     path: pageArgs.path,
-//     locale: pageArgs.locale,
-//     userId: req.user.id,
-//     isPrivate: false
-//   })
-
-//   pageArgs.tags = _.get(page, 'tags', [])
-
-//   if (versionId > 0) {
-//     if (!WIKI.auth.checkAccess(req.user, ['read:history'], pageArgs)) {
-//       _.set(res.locals, 'pageMeta.title', 'Unauthorized')
-//       return res.render('unauthorized', { action: 'downloadVersion' })
-//     }
-//   } else {
-//     if (!WIKI.auth.checkAccess(req.user, ['read:source'], pageArgs)) {
-//       _.set(res.locals, 'pageMeta.title', 'Unauthorized')
-//       return res.render('unauthorized', { action: 'download' })
-//     }
-//   }
-
-//   if (page) {
-//     const fileName = _.last(page.path.split('/')) + '.' + pageHelper.getFileExtension(page.contentType)
-//     res.attachment(fileName)
-//     if (versionId > 0) {
-//       const pageVersion = await WIKI.db.pageHistory.getVersion({ pageId: page.id, versionId })
-//       res.send(pageHelper.injectPageMetadata(pageVersion))
-//     } else {
-//       res.send(pageHelper.injectPageMetadata(page))
-//     }
-//   } else {
-//     res.status(404).end()
-//   }
-// })
-
-// /**
-//  * Create/Edit document
-//  */
-// router.get(['/_edit', '/_edit/*'], async (req, res, next) => {
-//   const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
-//   const site = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
-
-//   if (!site) {
-//     throw new Error('INVALID_SITE')
-//   }
-
-//   if (pageArgs.path === '') {
-//     return res.redirect(`/_edit/home`)
-//   }
-
-//   // if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
-//   //   return res.redirect(`/_edit/${pageArgs.locale}/${pageArgs.path}`)
-//   // }
-
-//   // req.i18n.changeLanguage(pageArgs.locale)
-
-//   // -> Set Editor Lang
-//   _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
-//   // _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
-
-//   // -> Check for reserved path
-//   if (pageHelper.isReservedPath(pageArgs.path)) {
-//     return next(new Error('Cannot create this page because it starts with a system reserved path.'))
-//   }
-
-//   // -> Get page data from DB
-//   let page = await WIKI.db.pages.getPageFromDb({
-//     siteId: site.id,
-//     path: pageArgs.path,
-//     locale: pageArgs.locale,
-//     userId: req.user.id
-//   })
-
-//   pageArgs.tags = _.get(page, 'tags', [])
-
-//   // -> Effective Permissions
-//   const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
-
-//   const injectCode = {
-//     css: '', // WIKI.config.theming.injectCSS,
-//     head: '', // WIKI.config.theming.injectHead,
-//     body: '' // WIKI.config.theming.injectBody
-//   }
-
-//   if (page) {
-//     // -> EDIT MODE
-//     if (!(effectivePermissions.pages.write || effectivePermissions.pages.manage)) {
-//       _.set(res.locals, 'pageMeta.title', 'Unauthorized')
-//       return res.render('unauthorized', { action: 'edit' })
-//     }
-
-//     // -> Get page tags
-//     await page.$relatedQuery('tags')
-//     page.tags = _.map(page.tags, 'tag')
-
-//     // Handle missing extra field
-//     page.extra = page.extra || { css: '', js: '' }
-
-//     // -> Beautify Script CSS
-//     if (!_.isEmpty(page.extra.css)) {
-//       page.extra.css = new CleanCSS({ format: 'beautify' }).minify(page.extra.css).styles
-//     }
-
-//     _.set(res.locals, 'pageMeta.title', `Edit ${page.title}`)
-//     _.set(res.locals, 'pageMeta.description', page.description)
-//     page.mode = 'update'
-//     page.isPublished = (page.isPublished === true || page.isPublished === 1) ? 'true' : 'false'
-//     page.content = Buffer.from(page.content).toString('base64')
-//   } else {
-//     // -> CREATE MODE
-//     if (!effectivePermissions.pages.write) {
-//       _.set(res.locals, 'pageMeta.title', 'Unauthorized')
-//       return res.render('unauthorized', { action: 'create' })
-//     }
-
-//     _.set(res.locals, 'pageMeta.title', `New Page`)
-//     page = {
-//       path: pageArgs.path,
-//       localeCode: pageArgs.locale,
-//       editorKey: null,
-//       mode: 'create',
-//       content: null,
-//       title: null,
-//       description: null,
-//       updatedAt: new Date().toISOString(),
-//       extra: {
-//         css: '',
-//         js: ''
-//       }
-//     }
-//   }
-
-//   res.render('editor', { page, injectCode, effectivePermissions })
-// })
-
-// /**
-//  * History
-//  */
-// router.get(['/h', '/h/*'], async (req, res, next) => {
-//   const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
-
-//   if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
-//     return res.redirect(`/h/${pageArgs.locale}/${pageArgs.path}`)
-//   }
-
-//   req.i18n.changeLanguage(pageArgs.locale)
-
-//   _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
-//   _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
-
-//   const page = await WIKI.db.pages.getPageFromDb({
-//     path: pageArgs.path,
-//     locale: pageArgs.locale,
-//     userId: req.user.id,
-//     isPrivate: false
-//   })
-
-//   if (!page) {
-//     _.set(res.locals, 'pageMeta.title', 'Page Not Found')
-//     return res.status(404).render('notfound', { action: 'history' })
-//   }
-
-//   pageArgs.tags = _.get(page, 'tags', [])
-
-//   const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
-
-//   if (!effectivePermissions.history.read) {
-//     _.set(res.locals, 'pageMeta.title', 'Unauthorized')
-//     return res.render('unauthorized', { action: 'history' })
-//   }
-
-//   if (page) {
-//     _.set(res.locals, 'pageMeta.title', page.title)
-//     _.set(res.locals, 'pageMeta.description', page.description)
-
-//     res.render('history', { page, effectivePermissions })
-//   } else {
-//     res.redirect(`/${pageArgs.path}`)
-//   }
-// })
-
-// /**
-//  * Page ID redirection
-//  */
-// router.get(['/i', '/i/:id'], async (req, res, next) => {
-//   const pageId = _.toSafeInteger(req.params.id)
-//   if (pageId <= 0) {
-//     return res.redirect('/')
-//   }
-
-//   const page = await WIKI.db.pages.query().column(['path', 'localeCode', 'isPrivate', 'privateNS']).findById(pageId)
-//   if (!page) {
-//     _.set(res.locals, 'pageMeta.title', 'Page Not Found')
-//     return res.status(404).render('notfound', { action: 'view' })
-//   }
-
-//   if (!WIKI.auth.checkAccess(req.user, ['read:pages'], {
-//     locale: page.localeCode,
-//     path: page.path,
-//     private: page.isPrivate,
-//     privateNS: page.privateNS,
-//     explicitLocale: false,
-//     tags: page.tags
-//   })) {
-//     _.set(res.locals, 'pageMeta.title', 'Unauthorized')
-//     return res.render('unauthorized', { action: 'view' })
-//   }
-
-//   if (WIKI.config.lang.namespacing) {
-//     return res.redirect(`/${page.localeCode}/${page.path}`)
-//   } else {
-//     return res.redirect(`/${page.path}`)
-//   }
-// })
-
-// /**
-//  * Source
-//  */
-// router.get(['/s', '/s/*'], async (req, res, next) => {
-//   const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
-//   const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0
-
-//   const page = await WIKI.db.pages.getPageFromDb({
-//     path: pageArgs.path,
-//     locale: pageArgs.locale,
-//     userId: req.user.id,
-//     isPrivate: false
-//   })
-
-//   pageArgs.tags = _.get(page, 'tags', [])
-
-//   if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
-//     return res.redirect(`/s/${pageArgs.locale}/${pageArgs.path}`)
-//   }
-
-//   // -> Effective Permissions
-//   const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
-
-//   _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
-//   _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
-
-//   if (versionId > 0) {
-//     if (!effectivePermissions.history.read) {
-//       _.set(res.locals, 'pageMeta.title', 'Unauthorized')
-//       return res.render('unauthorized', { action: 'sourceVersion' })
-//     }
-//   } else {
-//     if (!effectivePermissions.source.read) {
-//       _.set(res.locals, 'pageMeta.title', 'Unauthorized')
-//       return res.render('unauthorized', { action: 'source' })
-//     }
-//   }
-
-//   if (page) {
-//     if (versionId > 0) {
-//       const pageVersion = await WIKI.db.pageHistory.getVersion({ pageId: page.id, versionId })
-//       _.set(res.locals, 'pageMeta.title', pageVersion.title)
-//       _.set(res.locals, 'pageMeta.description', pageVersion.description)
-//       res.render('source', {
-//         page: {
-//           ...page,
-//           ...pageVersion
-//         },
-//         effectivePermissions
-//       })
-//     } else {
-//       _.set(res.locals, 'pageMeta.title', page.title)
-//       _.set(res.locals, 'pageMeta.description', page.description)
-
-//       res.render('source', { page, effectivePermissions })
-//     }
-//   } else {
-//     res.redirect(`/${pageArgs.path}`)
-//   }
-// })
-
-// /**
-//  * Tags
-//  */
-// router.get(['/t', '/t/*'], (req, res, next) => {
-//   _.set(res.locals, 'pageMeta.title', 'Tags')
-//   res.render('tags')
-// })
-
-/**
- * User Avatar
- */
-router.get('/_user/:uid/avatar', async (req, res, next) => {
-  if (!WIKI.auth.checkAccess(req.user, ['read:pages'])) {
-    return res.sendStatus(403)
-  }
-  const av = await WIKI.db.users.getUserAvatarData(req.params.uid)
-  if (av) {
-    res.set('Content-Type', 'image/jpeg')
-    return res.send(av)
-  }
-
-  return res.sendStatus(404)
-})
-
-// /**
-//  * View document / asset
-//  */
-// router.get('/*', async (req, res, next) => {
-//   const stripExt = _.some(WIKI.data.pageExtensions, ext => _.endsWith(req.path, `.${ext}`))
-//   const pageArgs = pageHelper.parsePath(req.path, { stripExt })
-//   const isPage = (stripExt || pageArgs.path.indexOf('.') === -1)
-//   const site = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
-
-//   if (!site) {
-//     throw new Error('INVALID_SITE')
-//   }
-
-//   if (isPage) {
-//     // if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
-//     //   return res.redirect(`/${pageArgs.locale}/${pageArgs.path}`)
-//     // }
-
-//     // req.i18n.changeLanguage(pageArgs.locale)
-
-//     try {
-//       // -> Get Page from cache
-//       const page = await WIKI.db.pages.getPage({
-//         siteId: site.id,
-//         path: pageArgs.path,
-//         locale: pageArgs.locale,
-//         userId: req.user.id
-//       })
-//       pageArgs.tags = _.get(page, 'tags', [])
-
-//       // -> Effective Permissions
-//       const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
-
-//       // -> Check User Access
-//       if (!effectivePermissions.pages.read) {
-//         if (req.user.id === WIKI.auth.guest.id) {
-//           res.cookie('loginRedirect', req.path, {
-//             maxAge: 15 * 60 * 1000
-//           })
-//         }
-//         if (pageArgs.path === 'home' && req.user.id === WIKI.auth.guest.id) {
-//           return res.redirect('/login')
-//         }
-//         return res.redirect(`/_error/unauthorized?from=${req.path}`)
-//       }
-
-//       _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
-//       // _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
-
-//       if (page) {
-//         _.set(res.locals, 'pageMeta.title', page.title)
-//         _.set(res.locals, 'pageMeta.description', page.description)
-
-//         // -> Check Publishing State
-//         let pageIsPublished = page.isPublished
-//         if (pageIsPublished && !_.isEmpty(page.publishStartDate)) {
-//           pageIsPublished = moment(page.publishStartDate).isSameOrBefore()
-//         }
-//         if (pageIsPublished && !_.isEmpty(page.publishEndDate)) {
-//           pageIsPublished = moment(page.publishEndDate).isSameOrAfter()
-//         }
-//         if (!pageIsPublished && !effectivePermissions.pages.write) {
-//           _.set(res.locals, 'pageMeta.title', 'Unauthorized')
-//           return res.status(403).render('unauthorized', {
-//             action: 'view'
-//           })
-//         }
-
-//         // -> Render view
-//         res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
-//       } else if (pageArgs.path === 'home') {
-//         res.redirect('/_welcome')
-//       } else {
-//         _.set(res.locals, 'pageMeta.title', 'Page Not Found')
-//         if (effectivePermissions.pages.write) {
-//           res.status(404).render('new', { path: pageArgs.path, locale: pageArgs.locale })
-//         } else {
-//           res.status(404).render('notfound', { action: 'view' })
-//         }
-//       }
-//     } catch (err) {
-//       next(err)
-//     }
-//   } else {
-//     if (!WIKI.auth.checkAccess(req.user, ['read:assets'], pageArgs)) {
-//       return res.sendStatus(403)
-//     }
-
-//     await WIKI.db.assets.getAsset(pageArgs.path, res)
-//   }
-// })
-
-router.get('/*', (req, res, next) => {
-  res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
-})
-
-module.exports = router

+ 534 - 0
server/controllers/common.mjs

@@ -0,0 +1,534 @@
+import express from 'express'
+// import pageHelper from '../helpers/page.mjs'
+// import CleanCSS from 'clean-css'
+import path from 'node:path'
+
+export default function () {
+  const router = express.Router()
+  const siteAssetsPath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'assets')
+
+  /**
+   * Robots.txt
+   */
+  router.get('/robots.txt', (req, res, next) => {
+    res.type('text/plain')
+    if (WIKI.config.seo.robots.includes('noindex')) {
+      res.send('User-agent: *\nDisallow: /')
+    } else {
+      res.status(200).end()
+    }
+  })
+
+  /**
+   * Health Endpoint
+   */
+  router.get('/healthz', (req, res, next) => {
+    if (WIKI.db.knex.client.pool.numFree() < 1 && WIKI.db.knex.client.pool.numUsed() < 1) {
+      res.status(503).json({ ok: false }).end()
+    } else {
+      res.status(200).json({ ok: true }).end()
+    }
+  })
+
+  /**
+   * Site Asset
+   */
+  router.get('/_site/:siteId?/:resource', async (req, res, next) => {
+    const site = req.params.siteId ? WIKI.sites[req.params.siteId] : await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
+    if (!site) {
+      return res.status(404).send('Site Not Found')
+    }
+    switch (req.params.resource) {
+      case 'logo': {
+        if (site.config.assets.logo) {
+          // TODO: Fetch from db if not in disk cache
+          res.sendFile(path.join(siteAssetsPath, `logo-${site.id}.${site.config.assets.logoExt}`))
+        } else {
+          res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/logo-wikijs.svg'))
+        }
+        break
+      }
+      case 'favicon': {
+        if (site.config.assets.favicon) {
+          // TODO: Fetch from db if not in disk cache
+          res.sendFile(path.join(siteAssetsPath, `favicon-${site.id}.${site.config.assets.faviconExt}`))
+        } else {
+          res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/logo-wikijs.svg'))
+        }
+        break
+      }
+      case 'loginbg': {
+        if (site.config.assets.loginBg) {
+          // TODO: Fetch from db if not in disk cache
+          res.sendFile(path.join(siteAssetsPath, `loginbg-${site.id}.jpg`))
+        } else {
+          res.sendFile(path.join(WIKI.ROOTPATH, 'assets/_assets/bg/login.jpg'))
+        }
+        break
+      }
+      default: {
+        return res.status(404).send('Invalid Site Resource')
+      }
+    }
+  })
+
+  /**
+   * Asset Thumbnails / Download
+   */
+  router.get('/_thumb/:id.webp', async (req, res, next) => {
+    const thumb = await WIKI.db.assets.getThumbnail({
+      id: req.params.id
+    })
+
+    if (thumb) {
+      // TODO: Check permissions
+
+      switch (thumb.previewState) {
+        case 'pending': {
+          res.redirect('/_assets/illustrations/fileman-pending.svg')
+          break
+        }
+        case 'ready': {
+          res.set('Content-Type', 'image/webp')
+          res.send(thumb.preview)
+          break
+        }
+        case 'failed': {
+          res.redirect('/_assets/illustrations/fileman-failed.svg')
+          break
+        }
+        default: {
+          return res.status(500).send('Invalid Thumbnail Preview State')
+        }
+      }
+    } else {
+      return res.sendStatus(404)
+    }
+  })
+
+  // router.get(['/_admin', '/_admin/*'], (req, res, next) => {
+  //   if (!WIKI.auth.checkAccess(req.user, [
+  //     'manage:system',
+  //     'write:users',
+  //     'manage:users',
+  //     'write:groups',
+  //     'manage:groups',
+  //     'manage:navigation',
+  //     'manage:theme',
+  //     'manage:api'
+  //   ])) {
+  //     _.set(res.locals, 'pageMeta.title', 'Unauthorized')
+  //     return res.status(403).render('unauthorized', { action: 'view' })
+  //   }
+
+  //   _.set(res.locals, 'pageMeta.title', 'Admin')
+  //   res.render('admin')
+
+  // })
+
+  // /**
+  //  * Download Page / Version
+  //  */
+  // router.get(['/d', '/d/*'], async (req, res, next) => {
+  //   const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
+
+  //   const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0
+
+  //   const page = await WIKI.db.pages.getPageFromDb({
+  //     path: pageArgs.path,
+  //     locale: pageArgs.locale,
+  //     userId: req.user.id,
+  //     isPrivate: false
+  //   })
+
+  //   pageArgs.tags = _.get(page, 'tags', [])
+
+  //   if (versionId > 0) {
+  //     if (!WIKI.auth.checkAccess(req.user, ['read:history'], pageArgs)) {
+  //       _.set(res.locals, 'pageMeta.title', 'Unauthorized')
+  //       return res.render('unauthorized', { action: 'downloadVersion' })
+  //     }
+  //   } else {
+  //     if (!WIKI.auth.checkAccess(req.user, ['read:source'], pageArgs)) {
+  //       _.set(res.locals, 'pageMeta.title', 'Unauthorized')
+  //       return res.render('unauthorized', { action: 'download' })
+  //     }
+  //   }
+
+  //   if (page) {
+  //     const fileName = _.last(page.path.split('/')) + '.' + pageHelper.getFileExtension(page.contentType)
+  //     res.attachment(fileName)
+  //     if (versionId > 0) {
+  //       const pageVersion = await WIKI.db.pageHistory.getVersion({ pageId: page.id, versionId })
+  //       res.send(pageHelper.injectPageMetadata(pageVersion))
+  //     } else {
+  //       res.send(pageHelper.injectPageMetadata(page))
+  //     }
+  //   } else {
+  //     res.status(404).end()
+  //   }
+  // })
+
+  // /**
+  //  * Create/Edit document
+  //  */
+  // router.get(['/_edit', '/_edit/*'], async (req, res, next) => {
+  //   const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
+  //   const site = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
+
+  //   if (!site) {
+  //     throw new Error('INVALID_SITE')
+  //   }
+
+  //   if (pageArgs.path === '') {
+  //     return res.redirect(`/_edit/home`)
+  //   }
+
+  //   // if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
+  //   //   return res.redirect(`/_edit/${pageArgs.locale}/${pageArgs.path}`)
+  //   // }
+
+  //   // req.i18n.changeLanguage(pageArgs.locale)
+
+  //   // -> Set Editor Lang
+  //   _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
+  //   // _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
+
+  //   // -> Check for reserved path
+  //   if (pageHelper.isReservedPath(pageArgs.path)) {
+  //     return next(new Error('Cannot create this page because it starts with a system reserved path.'))
+  //   }
+
+  //   // -> Get page data from DB
+  //   let page = await WIKI.db.pages.getPageFromDb({
+  //     siteId: site.id,
+  //     path: pageArgs.path,
+  //     locale: pageArgs.locale,
+  //     userId: req.user.id
+  //   })
+
+  //   pageArgs.tags = _.get(page, 'tags', [])
+
+  //   // -> Effective Permissions
+  //   const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
+
+  //   const injectCode = {
+  //     css: '', // WIKI.config.theming.injectCSS,
+  //     head: '', // WIKI.config.theming.injectHead,
+  //     body: '' // WIKI.config.theming.injectBody
+  //   }
+
+  //   if (page) {
+  //     // -> EDIT MODE
+  //     if (!(effectivePermissions.pages.write || effectivePermissions.pages.manage)) {
+  //       _.set(res.locals, 'pageMeta.title', 'Unauthorized')
+  //       return res.render('unauthorized', { action: 'edit' })
+  //     }
+
+  //     // -> Get page tags
+  //     await page.$relatedQuery('tags')
+  //     page.tags = _.map(page.tags, 'tag')
+
+  //     // Handle missing extra field
+  //     page.extra = page.extra || { css: '', js: '' }
+
+  //     // -> Beautify Script CSS
+  //     if (!_.isEmpty(page.extra.css)) {
+  //       page.extra.css = new CleanCSS({ format: 'beautify' }).minify(page.extra.css).styles
+  //     }
+
+  //     _.set(res.locals, 'pageMeta.title', `Edit ${page.title}`)
+  //     _.set(res.locals, 'pageMeta.description', page.description)
+  //     page.mode = 'update'
+  //     page.isPublished = (page.isPublished === true || page.isPublished === 1) ? 'true' : 'false'
+  //     page.content = Buffer.from(page.content).toString('base64')
+  //   } else {
+  //     // -> CREATE MODE
+  //     if (!effectivePermissions.pages.write) {
+  //       _.set(res.locals, 'pageMeta.title', 'Unauthorized')
+  //       return res.render('unauthorized', { action: 'create' })
+  //     }
+
+  //     _.set(res.locals, 'pageMeta.title', `New Page`)
+  //     page = {
+  //       path: pageArgs.path,
+  //       localeCode: pageArgs.locale,
+  //       editorKey: null,
+  //       mode: 'create',
+  //       content: null,
+  //       title: null,
+  //       description: null,
+  //       updatedAt: new Date().toISOString(),
+  //       extra: {
+  //         css: '',
+  //         js: ''
+  //       }
+  //     }
+  //   }
+
+  //   res.render('editor', { page, injectCode, effectivePermissions })
+  // })
+
+  // /**
+  //  * History
+  //  */
+  // router.get(['/h', '/h/*'], async (req, res, next) => {
+  //   const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
+
+  //   if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
+  //     return res.redirect(`/h/${pageArgs.locale}/${pageArgs.path}`)
+  //   }
+
+  //   req.i18n.changeLanguage(pageArgs.locale)
+
+  //   _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
+  //   _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
+
+  //   const page = await WIKI.db.pages.getPageFromDb({
+  //     path: pageArgs.path,
+  //     locale: pageArgs.locale,
+  //     userId: req.user.id,
+  //     isPrivate: false
+  //   })
+
+  //   if (!page) {
+  //     _.set(res.locals, 'pageMeta.title', 'Page Not Found')
+  //     return res.status(404).render('notfound', { action: 'history' })
+  //   }
+
+  //   pageArgs.tags = _.get(page, 'tags', [])
+
+  //   const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
+
+  //   if (!effectivePermissions.history.read) {
+  //     _.set(res.locals, 'pageMeta.title', 'Unauthorized')
+  //     return res.render('unauthorized', { action: 'history' })
+  //   }
+
+  //   if (page) {
+  //     _.set(res.locals, 'pageMeta.title', page.title)
+  //     _.set(res.locals, 'pageMeta.description', page.description)
+
+  //     res.render('history', { page, effectivePermissions })
+  //   } else {
+  //     res.redirect(`/${pageArgs.path}`)
+  //   }
+  // })
+
+  // /**
+  //  * Page ID redirection
+  //  */
+  // router.get(['/i', '/i/:id'], async (req, res, next) => {
+  //   const pageId = _.toSafeInteger(req.params.id)
+  //   if (pageId <= 0) {
+  //     return res.redirect('/')
+  //   }
+
+  //   const page = await WIKI.db.pages.query().column(['path', 'localeCode', 'isPrivate', 'privateNS']).findById(pageId)
+  //   if (!page) {
+  //     _.set(res.locals, 'pageMeta.title', 'Page Not Found')
+  //     return res.status(404).render('notfound', { action: 'view' })
+  //   }
+
+  //   if (!WIKI.auth.checkAccess(req.user, ['read:pages'], {
+  //     locale: page.localeCode,
+  //     path: page.path,
+  //     private: page.isPrivate,
+  //     privateNS: page.privateNS,
+  //     explicitLocale: false,
+  //     tags: page.tags
+  //   })) {
+  //     _.set(res.locals, 'pageMeta.title', 'Unauthorized')
+  //     return res.render('unauthorized', { action: 'view' })
+  //   }
+
+  //   if (WIKI.config.lang.namespacing) {
+  //     return res.redirect(`/${page.localeCode}/${page.path}`)
+  //   } else {
+  //     return res.redirect(`/${page.path}`)
+  //   }
+  // })
+
+  // /**
+  //  * Source
+  //  */
+  // router.get(['/s', '/s/*'], async (req, res, next) => {
+  //   const pageArgs = pageHelper.parsePath(req.path, { stripExt: true })
+  //   const versionId = (req.query.v) ? _.toSafeInteger(req.query.v) : 0
+
+  //   const page = await WIKI.db.pages.getPageFromDb({
+  //     path: pageArgs.path,
+  //     locale: pageArgs.locale,
+  //     userId: req.user.id,
+  //     isPrivate: false
+  //   })
+
+  //   pageArgs.tags = _.get(page, 'tags', [])
+
+  //   if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
+  //     return res.redirect(`/s/${pageArgs.locale}/${pageArgs.path}`)
+  //   }
+
+  //   // -> Effective Permissions
+  //   const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
+
+  //   _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
+  //   _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
+
+  //   if (versionId > 0) {
+  //     if (!effectivePermissions.history.read) {
+  //       _.set(res.locals, 'pageMeta.title', 'Unauthorized')
+  //       return res.render('unauthorized', { action: 'sourceVersion' })
+  //     }
+  //   } else {
+  //     if (!effectivePermissions.source.read) {
+  //       _.set(res.locals, 'pageMeta.title', 'Unauthorized')
+  //       return res.render('unauthorized', { action: 'source' })
+  //     }
+  //   }
+
+  //   if (page) {
+  //     if (versionId > 0) {
+  //       const pageVersion = await WIKI.db.pageHistory.getVersion({ pageId: page.id, versionId })
+  //       _.set(res.locals, 'pageMeta.title', pageVersion.title)
+  //       _.set(res.locals, 'pageMeta.description', pageVersion.description)
+  //       res.render('source', {
+  //         page: {
+  //           ...page,
+  //           ...pageVersion
+  //         },
+  //         effectivePermissions
+  //       })
+  //     } else {
+  //       _.set(res.locals, 'pageMeta.title', page.title)
+  //       _.set(res.locals, 'pageMeta.description', page.description)
+
+  //       res.render('source', { page, effectivePermissions })
+  //     }
+  //   } else {
+  //     res.redirect(`/${pageArgs.path}`)
+  //   }
+  // })
+
+  // /**
+  //  * Tags
+  //  */
+  // router.get(['/t', '/t/*'], (req, res, next) => {
+  //   _.set(res.locals, 'pageMeta.title', 'Tags')
+  //   res.render('tags')
+  // })
+
+  /**
+   * User Avatar
+   */
+  router.get('/_user/:uid/avatar', async (req, res, next) => {
+    if (!WIKI.auth.checkAccess(req.user, ['read:pages'])) {
+      return res.sendStatus(403)
+    }
+    const av = await WIKI.db.users.getUserAvatarData(req.params.uid)
+    if (av) {
+      res.set('Content-Type', 'image/jpeg')
+      return res.send(av)
+    }
+
+    return res.sendStatus(404)
+  })
+
+  // /**
+  //  * View document / asset
+  //  */
+  // router.get('/*', async (req, res, next) => {
+  //   const stripExt = _.some(WIKI.data.pageExtensions, ext => _.endsWith(req.path, `.${ext}`))
+  //   const pageArgs = pageHelper.parsePath(req.path, { stripExt })
+  //   const isPage = (stripExt || pageArgs.path.indexOf('.') === -1)
+  //   const site = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
+
+  //   if (!site) {
+  //     throw new Error('INVALID_SITE')
+  //   }
+
+  //   if (isPage) {
+  //     // if (WIKI.config.lang.namespacing && !pageArgs.explicitLocale) {
+  //     //   return res.redirect(`/${pageArgs.locale}/${pageArgs.path}`)
+  //     // }
+
+  //     // req.i18n.changeLanguage(pageArgs.locale)
+
+  //     try {
+  //       // -> Get Page from cache
+  //       const page = await WIKI.db.pages.getPage({
+  //         siteId: site.id,
+  //         path: pageArgs.path,
+  //         locale: pageArgs.locale,
+  //         userId: req.user.id
+  //       })
+  //       pageArgs.tags = _.get(page, 'tags', [])
+
+  //       // -> Effective Permissions
+  //       const effectivePermissions = WIKI.auth.getEffectivePermissions(req, pageArgs)
+
+  //       // -> Check User Access
+  //       if (!effectivePermissions.pages.read) {
+  //         if (req.user.id === WIKI.auth.guest.id) {
+  //           res.cookie('loginRedirect', req.path, {
+  //             maxAge: 15 * 60 * 1000
+  //           })
+  //         }
+  //         if (pageArgs.path === 'home' && req.user.id === WIKI.auth.guest.id) {
+  //           return res.redirect('/login')
+  //         }
+  //         return res.redirect(`/_error/unauthorized?from=${req.path}`)
+  //       }
+
+  //       _.set(res, 'locals.siteConfig.lang', pageArgs.locale)
+  //       // _.set(res, 'locals.siteConfig.rtl', req.i18n.dir() === 'rtl')
+
+  //       if (page) {
+  //         _.set(res.locals, 'pageMeta.title', page.title)
+  //         _.set(res.locals, 'pageMeta.description', page.description)
+
+  //         // -> Check Publishing State
+  //         let pageIsPublished = page.isPublished
+  //         if (pageIsPublished && !_.isEmpty(page.publishStartDate)) {
+  //           pageIsPublished = moment(page.publishStartDate).isSameOrBefore()
+  //         }
+  //         if (pageIsPublished && !_.isEmpty(page.publishEndDate)) {
+  //           pageIsPublished = moment(page.publishEndDate).isSameOrAfter()
+  //         }
+  //         if (!pageIsPublished && !effectivePermissions.pages.write) {
+  //           _.set(res.locals, 'pageMeta.title', 'Unauthorized')
+  //           return res.status(403).render('unauthorized', {
+  //             action: 'view'
+  //           })
+  //         }
+
+  //         // -> Render view
+  //         res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
+  //       } else if (pageArgs.path === 'home') {
+  //         res.redirect('/_welcome')
+  //       } else {
+  //         _.set(res.locals, 'pageMeta.title', 'Page Not Found')
+  //         if (effectivePermissions.pages.write) {
+  //           res.status(404).render('new', { path: pageArgs.path, locale: pageArgs.locale })
+  //         } else {
+  //           res.status(404).render('notfound', { action: 'view' })
+  //         }
+  //       }
+  //     } catch (err) {
+  //       next(err)
+  //     }
+  //   } else {
+  //     if (!WIKI.auth.checkAccess(req.user, ['read:assets'], pageArgs)) {
+  //       return res.sendStatus(403)
+  //     }
+
+  //     await WIKI.db.assets.getAsset(pageArgs.path, res)
+  //   }
+  // })
+
+  router.get('/*', (req, res, next) => {
+    res.sendFile(path.join(WIKI.ROOTPATH, 'assets/index.html'))
+  })
+
+  return router
+}

+ 0 - 36
server/controllers/ssl.js

@@ -1,36 +0,0 @@
-const express = require('express')
-const router = express.Router()
-const _ = require('lodash')
-const qs = require('querystring')
-
-/**
- * Let's Encrypt Challenge
- */
-router.get('/.well-known/acme-challenge/:token', (req, res, next) => {
-  res.type('text/plain')
-  if (_.get(WIKI.config, 'letsencrypt.challenge', false)) {
-    if (WIKI.config.letsencrypt.challenge.token === req.params.token) {
-      res.send(WIKI.config.letsencrypt.challenge.keyAuthorization)
-      WIKI.logger.info(`(LETSENCRYPT) Received valid challenge request. [ ACCEPTED ]`)
-    } else {
-      res.status(406).send('Invalid Challenge Token!')
-      WIKI.logger.warn(`(LETSENCRYPT) Received invalid challenge request. [ REJECTED ]`)
-    }
-  } else {
-    res.status(418).end()
-  }
-})
-
-/**
- * Redirect to HTTPS if HTTP Redirection is enabled
- */
-// router.all('/*', (req, res, next) => {
-//   if (WIKI.config.server.sslRedir && !req.secure && WIKI.servers.servers.https) {
-//     let query = (!_.isEmpty(req.query)) ? `?${qs.stringify(req.query)}` : ``
-//     return res.redirect(`https://${req.hostname}${req.originalUrl}${query}`)
-//   } else {
-//     next()
-//   }
-// })
-
-module.exports = router

+ 39 - 0
server/controllers/ssl.mjs

@@ -0,0 +1,39 @@
+import express from 'express'
+import { get } from 'lodash-es'
+import qs from 'querystring'
+
+export default function () {
+  const router = express.Router()
+
+  /**
+   * Let's Encrypt Challenge
+   */
+  router.get('/.well-known/acme-challenge/:token', (req, res, next) => {
+    res.type('text/plain')
+    if (get(WIKI.config, 'letsencrypt.challenge', false)) {
+      if (WIKI.config.letsencrypt.challenge.token === req.params.token) {
+        res.send(WIKI.config.letsencrypt.challenge.keyAuthorization)
+        WIKI.logger.info(`(LETSENCRYPT) Received valid challenge request. [ ACCEPTED ]`)
+      } else {
+        res.status(406).send('Invalid Challenge Token!')
+        WIKI.logger.warn(`(LETSENCRYPT) Received invalid challenge request. [ REJECTED ]`)
+      }
+    } else {
+      res.status(418).end()
+    }
+  })
+
+  /**
+   * Redirect to HTTPS if HTTP Redirection is enabled
+   */
+  // router.all('/*', (req, res, next) => {
+  //   if (WIKI.config.server.sslRedir && !req.secure && WIKI.servers.servers.https) {
+  //     let query = (!_.isEmpty(req.query)) ? `?${qs.stringify(req.query)}` : ``
+  //     return res.redirect(`https://${req.hostname}${req.originalUrl}${query}`)
+  //   } else {
+  //     next()
+  //   }
+  // })
+
+  return router
+}

+ 0 - 105
server/controllers/upload.js

@@ -1,105 +0,0 @@
-const express = require('express')
-const router = express.Router()
-const _ = require('lodash')
-const multer = require('multer')
-const path = require('path')
-const sanitize = require('sanitize-filename')
-
-/**
- * Upload files
- */
-router.post('/u', (req, res, next) => {
-  multer({
-    dest: path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'),
-    limits: {
-      fileSize: WIKI.config.uploads.maxFileSize,
-      files: WIKI.config.uploads.maxFiles
-    }
-  }).array('mediaUpload')(req, res, next)
-}, async (req, res, next) => {
-  if (!_.some(req.user.permissions, pm => _.includes(['write:assets', 'manage:system'], pm))) {
-    return res.status(403).json({
-      succeeded: false,
-      message: 'You are not authorized to upload files.'
-    })
-  } else if (req.files.length < 1) {
-    return res.status(400).json({
-      succeeded: false,
-      message: 'Missing upload payload.'
-    })
-  } else if (req.files.length > 1) {
-    return res.status(400).json({
-      succeeded: false,
-      message: 'You cannot upload multiple files within the same request.'
-    })
-  }
-  const fileMeta = _.get(req, 'files[0]', false)
-  if (!fileMeta) {
-    return res.status(500).json({
-      succeeded: false,
-      message: 'Missing upload file metadata.'
-    })
-  }
-
-  // Get folder Id
-  let folderId = null
-  try {
-    const folderRaw = _.get(req, 'body.mediaUpload', false)
-    if (folderRaw) {
-      folderId = _.get(JSON.parse(folderRaw), 'folderId', null)
-      if (folderId === 0) {
-        folderId = null
-      }
-    } else {
-      throw new Error('Missing File Metadata')
-    }
-  } catch (err) {
-    return res.status(400).json({
-      succeeded: false,
-      message: 'Missing upload folder metadata.'
-    })
-  }
-
-  // Build folder hierarchy
-  let hierarchy = []
-  if (folderId) {
-    try {
-      hierarchy = await WIKI.db.assetFolders.getHierarchy(folderId)
-    } catch (err) {
-      return res.status(400).json({
-        succeeded: false,
-        message: 'Failed to fetch folder hierarchy.'
-      })
-    }
-  }
-
-  // Sanitize filename
-  fileMeta.originalname = sanitize(fileMeta.originalname.toLowerCase().replace(/[\s,;#]+/g, '_'))
-
-  // Check if user can upload at path
-  const assetPath = (folderId) ? hierarchy.map(h => h.slug).join('/') + `/${fileMeta.originalname}` : fileMeta.originalname
-  if (!WIKI.auth.checkAccess(req.user, ['write:assets'], { path: assetPath })) {
-    return res.status(403).json({
-      succeeded: false,
-      message: 'You are not authorized to upload files to this folder.'
-    })
-  }
-
-  // Process upload file
-  await WIKI.db.assets.upload({
-    ...fileMeta,
-    mode: 'upload',
-    folderId: folderId,
-    assetPath,
-    user: req.user
-  })
-  res.send('ok')
-})
-
-router.get('/u', async (req, res, next) => {
-  res.json({
-    ok: true
-  })
-})
-
-module.exports = router

+ 5 - 5
server/controllers/ws.js → server/controllers/ws.mjs

@@ -1,16 +1,16 @@
-const chalk = require('chalk')
-const os = require('node:os')
+import chalk from 'chalk'
+import os from 'node:os'
 
-module.exports = () => {
+export default function () {
   WIKI.servers.ws.on('connection', (socket) => {
     // TODO: Validate token + permissions
     const token = socket.handshake.auth.token
-    console.info(token)
+    // console.info(token)
 
     const listeners = {}
 
     socket.on('server:logs', () => {
-      socket.emit('server:log', chalk`{greenBright Streaming logs from {bold Wiki.js} instance {yellowBright.bold ${WIKI.INSTANCE_ID}} on host {yellowBright.bold ${os.hostname()}}...}`)
+      socket.emit('server:log', chalk.greenBright(`Streaming logs from ${chalk.bold('Wiki.js')} instance ${chalk.yellowBright.bold(WIKI.INSTANCE_ID)} on host ${chalk.yellowBright.bold(os.hostname())}...`))
       listeners.serverLogs = (msg) => {
         socket.emit('server:log', msg)
       }

+ 8 - 9
server/core/asar.js → server/core/asar.mjs

@@ -1,20 +1,19 @@
-const pickle = require('chromium-pickle-js')
-const path = require('path')
-const UINT64 = require('cuint').UINT64
-const fs = require('fs')
+import pickle from 'chromium-pickle-js'
+import path from 'node:path'
+import { UINT64 } from 'cuint'
+import fs from 'node:fs'
 
 /**
  * Based of express-serve-asar (https://github.com/toyobayashi/express-serve-asar)
  * by Fenglin Li (https://github.com/toyobayashi)
  */
 
-const packages = {
-  'twemoji': path.join(WIKI.ROOTPATH, `assets-legacy/svg/twemoji.asar`)
-}
-
-module.exports = {
+export default {
   fdCache: {},
   async serve (pkgName, req, res, next) {
+    const packages = {
+      'twemoji': path.join(WIKI.ROOTPATH, `assets/svg/twemoji.asar`)
+    }
     const file = this.readFilesystemSync(packages[pkgName])
     const { filesystem, fd } = file
     const info = filesystem.getFile(req.path.substring(1))

+ 16 - 15
server/core/auth.js → server/core/auth.mjs

@@ -1,24 +1,25 @@
-const passport = require('passport')
-const passportJWT = require('passport-jwt')
-const _ = require('lodash')
-const jwt = require('jsonwebtoken')
-const ms = require('ms')
-const { DateTime } = require('luxon')
-const util = require('node:util')
-const crypto = require('node:crypto')
-const randomBytes = util.promisify(crypto.randomBytes)
-const pem2jwk = require('pem-jwk').pem2jwk
+import passport from 'passport'
+import passportJWT from 'passport-jwt'
+import _ from 'lodash'
+import jwt from 'jsonwebtoken'
+import ms from 'ms'
+import { DateTime } from 'luxon'
+import util from 'node:util'
+import crypto from 'node:crypto'
+import { pem2jwk } from 'pem-jwk'
+import NodeCache from 'node-cache'
+import { extractJWT } from '../helpers/security.mjs'
 
-const securityHelper = require('../helpers/security')
+const randomBytes = util.promisify(crypto.randomBytes)
 
-module.exports = {
+export default {
   strategies: {},
   guest: {
     cacheExpiration: DateTime.utc().minus({ days: 1 })
   },
   groups: {},
   validApiKeys: [],
-  revocationList: require('./cache').init(),
+  revocationList: new NodeCache(),
 
   /**
    * Initialize the authentication module
@@ -64,7 +65,7 @@ module.exports = {
 
       // Load JWT
       passport.use('jwt', new passportJWT.Strategy({
-        jwtFromRequest: securityHelper.extractJWT,
+        jwtFromRequest: extractJWT,
         secretOrKey: WIKI.config.auth.certs.public,
         audience: WIKI.config.auth.audience,
         issuer: 'urn:wiki.js',
@@ -141,7 +142,7 @@ module.exports = {
 
       // Revalidate and renew token
       if (mustRevalidate) {
-        const jwtPayload = jwt.decode(securityHelper.extractJWT(req))
+        const jwtPayload = jwt.decode(extractJWT(req))
         try {
           const newToken = await WIKI.db.users.refreshToken(jwtPayload.id, jwtPayload.pvd)
           user = newToken.user

+ 0 - 7
server/core/cache.js

@@ -1,7 +0,0 @@
-const NodeCache = require('node-cache')
-
-module.exports = {
-  init() {
-    return new NodeCache()
-  }
-}

+ 22 - 22
server/core/config.js → server/core/config.mjs

@@ -1,19 +1,19 @@
-const _ = require('lodash')
-const chalk = require('chalk')
-const cfgHelper = require('../helpers/config')
-const fs = require('fs')
-const path = require('path')
-const yaml = require('js-yaml')
-
-module.exports = {
+import { defaultsDeep, get, isPlainObject } from 'lodash-es'
+import chalk from 'chalk'
+import cfgHelper from '../helpers/config.mjs'
+import regexData from '../app/regex.mjs'
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import yaml from 'js-yaml'
+
+export default {
   /**
    * Load root config from disk
    */
-  init(silent = false) {
-    let confPaths = {
+  async init(silent = false) {
+    const confPaths = {
       config: path.join(WIKI.ROOTPATH, 'config.yml'),
-      data: path.join(WIKI.SERVERPATH, 'app/data.yml'),
-      dataRegex: path.join(WIKI.SERVERPATH, 'app/regex.js')
+      data: path.join(WIKI.SERVERPATH, 'app/data.yml')
     }
 
     if (process.env.dockerdev) {
@@ -34,11 +34,11 @@ module.exports = {
     try {
       appconfig = yaml.load(
         cfgHelper.parseConfigValue(
-          fs.readFileSync(confPaths.config, 'utf8')
+          await fs.readFile(confPaths.config, 'utf8')
         )
       )
-      appdata = yaml.load(fs.readFileSync(confPaths.data, 'utf8'))
-      appdata.regex = require(confPaths.dataRegex)
+      appdata = yaml.load(await fs.readFile(confPaths.data, 'utf8'))
+      appdata.regex = regexData
       if (!silent) {
         console.info(chalk.green.bold(`OK`))
       }
@@ -52,7 +52,7 @@ module.exports = {
 
     // Merge with defaults
 
-    appconfig = _.defaultsDeep(appconfig, appdata.defaults.config)
+    appconfig = defaultsDeep(appconfig, appdata.defaults.config)
 
     // Override port
 
@@ -66,7 +66,7 @@ module.exports = {
 
     // Load package info
 
-    const packageInfo = require(path.join(WIKI.SERVERPATH, 'package.json'))
+    const packageInfo = JSON.parse(await fs.readFile(path.join(WIKI.SERVERPATH, 'package.json'), 'utf-8'))
 
     // Load DB Password from Docker Secret File
     if (process.env.DB_PASS_FILE) {
@@ -74,7 +74,7 @@ module.exports = {
         console.info(chalk.blue(`DB_PASS_FILE is defined. Will use secret from file.`))
       }
       try {
-        appconfig.db.pass = fs.readFileSync(process.env.DB_PASS_FILE, 'utf8').trim()
+        appconfig.db.pass = await fs.readFile(process.env.DB_PASS_FILE, 'utf8').trim()
       } catch (err) {
         console.error(chalk.red.bold(`>>> Failed to read Docker Secret File using path defined in DB_PASS_FILE env variable!`))
         console.error(err.message)
@@ -93,9 +93,9 @@ module.exports = {
    * Load config from DB
    */
   async loadFromDb() {
-    let conf = await WIKI.db.settings.getConfig()
+    const conf = await WIKI.db.settings.getConfig()
     if (conf) {
-      WIKI.config = _.defaultsDeep(conf, WIKI.config)
+      WIKI.config = defaultsDeep(conf, WIKI.config)
     } else {
       WIKI.logger.warn('Missing DB Configuration!')
       process.exit(1)
@@ -110,8 +110,8 @@ module.exports = {
   async saveToDb(keys, propagate = true) {
     try {
       for (let key of keys) {
-        let value = _.get(WIKI.config, key, null)
-        if (!_.isPlainObject(value)) {
+        let value = get(WIKI.config, key, null)
+        if (!isPlainObject(value)) {
           value = { v: value }
         }
         let affectedRows = await WIKI.db.settings.query().patch({ value }).where('key', key)

+ 25 - 26
server/core/db.js → server/core/db.mjs

@@ -1,19 +1,18 @@
-const _ = require('lodash')
-const autoload = require('auto-load')
-const path = require('path')
-const Knex = require('knex')
-const fs = require('fs')
-const Objection = require('objection')
-const PGPubSub = require('pg-pubsub')
-
-const migrationSource = require('../db/migrator-source')
-const migrateFromLegacy = require('../db/legacy')
-const { setTimeout } = require('timers/promises')
+import { get, has, isEmpty, isPlainObject } from 'lodash-es'
+import path from 'node:path'
+import knex from 'knex'
+import fs from 'node:fs/promises'
+import Objection from 'objection'
+import PGPubSub from 'pg-pubsub'
+
+import migrationSource from '../db/migrator-source.mjs'
+// const migrateFromLegacy = require('../db/legacy')
+import { setTimeout } from 'node:timers/promises'
 
 /**
  * ORM DB module
  */
-module.exports = {
+export default {
   Objection,
   knex: null,
   listener: null,
@@ -21,14 +20,14 @@ module.exports = {
   /**
    * Initialize DB
    */
-  init(workerMode = false) {
+  async init (workerMode = false) {
     let self = this
 
     WIKI.logger.info('Checking DB configuration...')
 
     // Fetch DB Config
 
-    this.config = (!_.isEmpty(process.env.DATABASE_URL)) ? process.env.DATABASE_URL : {
+    this.config = (!isEmpty(process.env.DATABASE_URL)) ? process.env.DATABASE_URL : {
       host: WIKI.config.db.host.toString(),
       user: WIKI.config.db.user.toString(),
       password: WIKI.config.db.pass.toString(),
@@ -40,27 +39,27 @@ module.exports = {
 
     let dbUseSSL = (WIKI.config.db.ssl === true || WIKI.config.db.ssl === 'true' || WIKI.config.db.ssl === 1 || WIKI.config.db.ssl === '1')
     let sslOptions = null
-    if (dbUseSSL && _.isPlainObject(this.config) && _.get(WIKI.config.db, 'sslOptions.auto', null) === false) {
+    if (dbUseSSL && isPlainObject(this.config) && get(WIKI.config.db, 'sslOptions.auto', null) === false) {
       sslOptions = WIKI.config.db.sslOptions
       sslOptions.rejectUnauthorized = sslOptions.rejectUnauthorized !== false
       if (sslOptions.ca && sslOptions.ca.indexOf('-----') !== 0) {
-        sslOptions.ca = fs.readFileSync(path.resolve(WIKI.ROOTPATH, sslOptions.ca))
+        sslOptions.ca = await fs.readFile(path.resolve(WIKI.ROOTPATH, sslOptions.ca), 'utf-8')
       }
       if (sslOptions.cert) {
-        sslOptions.cert = fs.readFileSync(path.resolve(WIKI.ROOTPATH, sslOptions.cert))
+        sslOptions.cert = await fs.readFile(path.resolve(WIKI.ROOTPATH, sslOptions.cert), 'utf-8')
       }
       if (sslOptions.key) {
-        sslOptions.key = fs.readFileSync(path.resolve(WIKI.ROOTPATH, sslOptions.key))
+        sslOptions.key = await fs.readFile(path.resolve(WIKI.ROOTPATH, sslOptions.key), 'utf-8')
       }
       if (sslOptions.pfx) {
-        sslOptions.pfx = fs.readFileSync(path.resolve(WIKI.ROOTPATH, sslOptions.pfx))
+        sslOptions.pfx = await fs.readFile(path.resolve(WIKI.ROOTPATH, sslOptions.pfx), 'utf-8')
       }
     } else {
       sslOptions = true
     }
 
     // Handle inline SSL CA Certificate mode
-    if (!_.isEmpty(process.env.DB_SSL_CA)) {
+    if (!isEmpty(process.env.DB_SSL_CA)) {
       const chunks = []
       for (let i = 0, charsLength = process.env.DB_SSL_CA.length; i < charsLength; i += 64) {
         chunks.push(process.env.DB_SSL_CA.substring(i, i + 64))
@@ -73,12 +72,12 @@ module.exports = {
       }
     }
 
-    if (dbUseSSL && _.isPlainObject(this.config)) {
+    if (dbUseSSL && isPlainObject(this.config)) {
       this.config.ssl = (sslOptions === true) ? { rejectUnauthorized: true } : sslOptions
     }
 
     // Initialize Knex
-    this.knex = Knex({
+    this.knex = knex({
       client: 'pg',
       useNullAsDefault: true,
       asyncStackTraces: WIKI.IS_DEBUG,
@@ -104,11 +103,11 @@ module.exports = {
     // Load DB Models
 
     WIKI.logger.info('Loading DB models...')
-    const models = autoload(path.join(WIKI.SERVERPATH, 'models'))
+    const models = (await import(path.join(WIKI.SERVERPATH, 'models/index.mjs'))).default
 
     // Set init tasks
     let conAttempts = 0
-    let initTasks = {
+    const initTasks = {
       // -> Attempt initial connection
       async connect () {
         try {
@@ -143,7 +142,7 @@ module.exports = {
       },
       // -> Migrate DB Schemas from 2.x
       async migrateFromLegacy () {
-        return migrateFromLegacy.migrate(self.knex)
+        // return migrateFromLegacy.migrate(self.knex)
       }
     }
 
@@ -184,7 +183,7 @@ module.exports = {
     // -> Outbound events handling
 
     this.listener.addChannel('wiki', payload => {
-      if (_.has(payload, 'event') && payload.source !== WIKI.INSTANCE_ID) {
+      if (has(payload, 'event') && payload.source !== WIKI.INSTANCE_ID) {
         WIKI.logger.info(`Received event ${payload.event} from instance ${payload.source}: [ OK ]`)
         WIKI.events.inbound.emit(payload.event, payload.value)
       }

+ 5 - 5
server/core/extensions.js → server/core/extensions.mjs

@@ -1,13 +1,13 @@
-const fs = require('fs-extra')
-const path = require('path')
+import fs from 'node:fs/promises'
+import path from 'path'
 
-module.exports = {
+export default {
   ext: {},
   async init () {
     const extDirs = await fs.readdir(path.join(WIKI.SERVERPATH, 'modules/extensions'))
     WIKI.logger.info(`Checking for installed optional extensions...`)
-    for (let dir of extDirs) {
-      WIKI.extensions.ext[dir] = require(path.join(WIKI.SERVERPATH, 'modules/extensions', dir, 'ext.js'))
+    for (const dir of extDirs) {
+      WIKI.extensions.ext[dir] = (await import(path.join(WIKI.SERVERPATH, 'modules/extensions', dir, 'ext.mjs'))).default
       const isInstalled = await WIKI.extensions.ext[dir].check()
       if (isInstalled) {
         WIKI.logger.info(`Optional extension ${dir} is installed. [ OK ]`)

+ 21 - 13
server/core/kernel.js → server/core/kernel.mjs

@@ -1,16 +1,24 @@
-const _ = require('lodash')
-const EventEmitter = require('eventemitter2').EventEmitter2
+import { padEnd } from 'lodash-es'
+import eventemitter2 from 'eventemitter2'
+import NodeCache from 'node-cache'
+
+import asar from './asar.mjs'
+import db from './db.mjs'
+import extensions from './extensions.mjs'
+import scheduler from './scheduler.mjs'
+import servers from './servers.mjs'
 
 let isShuttingDown = false
 
-module.exports = {
+export default {
   async init() {
     WIKI.logger.info('=======================================')
-    WIKI.logger.info(`= Wiki.js ${_.padEnd(WIKI.version + ' ', 29, '=')}`)
+    WIKI.logger.info(`= Wiki.js ${padEnd(WIKI.version + ' ', 29, '=')}`)
     WIKI.logger.info('=======================================')
     WIKI.logger.info('Initializing...')
+    WIKI.logger.info(`Running node.js ${process.version}`)
 
-    WIKI.db = require('./db').init()
+    WIKI.db = await db.init()
 
     try {
       await WIKI.db.onReady
@@ -31,15 +39,15 @@ module.exports = {
    */
   async preBootWeb() {
     try {
-      WIKI.cache = require('./cache').init()
-      WIKI.scheduler = await require('./scheduler').init()
-      WIKI.servers = require('./servers')
+      WIKI.cache = new NodeCache()
+      WIKI.scheduler = await scheduler.init()
+      WIKI.servers = servers
       WIKI.events = {
-        inbound: new EventEmitter(),
-        outbound: new EventEmitter()
+        inbound: new eventemitter2.EventEmitter2(),
+        outbound: new eventemitter2.EventEmitter2()
       }
-      WIKI.extensions = require('./extensions')
-      WIKI.asar = require('./asar')
+      WIKI.extensions = extensions
+      WIKI.asar = asar
     } catch (err) {
       WIKI.logger.error(err)
       process.exit(1)
@@ -51,7 +59,7 @@ module.exports = {
   async bootWeb() {
     try {
       await this.preBootWeb()
-      await require('../web')()
+      await (await import('../web.mjs')).init()
       this.postBootWeb()
     } catch (err) {
       WIKI.logger.error(err)

+ 0 - 60
server/core/logger.js

@@ -1,60 +0,0 @@
-const chalk = require('chalk')
-const EventEmitter = require('events')
-
-const LEVELS = ['error', 'warn', 'info', 'debug']
-const LEVELSIGNORED = ['verbose', 'silly']
-const LEVELCOLORS = {
-  error: 'red',
-  warn: 'yellow',
-  info: 'green',
-  debug: 'cyan'
-}
-
-class Logger extends EventEmitter {}
-const primaryLogger = new Logger()
-
-let ignoreNextLevels = false
-
-primaryLogger.ws = new EventEmitter()
-
-LEVELS.forEach(lvl => {
-  primaryLogger[lvl] = (...args) => {
-    primaryLogger.emit(lvl, ...args)
-  }
-
-  if (!ignoreNextLevels) {
-    primaryLogger.on(lvl, (msg) => {
-      let formatted = ''
-      if (WIKI.config.logFormat === 'json') {
-        formatted = JSON.stringify({
-          timestamp: new Date().toISOString(),
-          instance: WIKI.INSTANCE_ID,
-          level: lvl,
-          message: msg
-        })
-      } else {
-        if (msg instanceof Error) {
-          msg = msg.stack
-        }
-        formatted = chalk`${new Date().toISOString()} {dim [${WIKI.INSTANCE_ID}]} {${LEVELCOLORS[lvl]}.bold ${lvl}}: ${msg}`
-      }
-
-      console.log(formatted)
-      primaryLogger.ws.emit('log', formatted)
-    })
-  }
-  if (lvl === WIKI.config.logLevel) {
-    ignoreNextLevels = true
-  }
-})
-
-LEVELSIGNORED.forEach(lvl => {
-  primaryLogger[lvl] = () => {}
-})
-
-module.exports = {
-  loggers: {},
-  init () {
-    return primaryLogger
-  }
-}

+ 61 - 0
server/core/logger.mjs

@@ -0,0 +1,61 @@
+import chalk from 'chalk'
+import EventEmitter from 'node:events'
+
+const LEVELS = ['error', 'warn', 'info', 'debug']
+const LEVELSIGNORED = ['verbose', 'silly']
+const LEVELCOLORS = {
+  error: 'red',
+  warn: 'yellow',
+  info: 'green',
+  debug: 'cyan'
+}
+
+class Logger extends EventEmitter {}
+
+export default {
+  loggers: {},
+  init () {
+    const primaryLogger = new Logger()
+
+    let ignoreNextLevels = false
+
+    primaryLogger.ws = new EventEmitter()
+
+    LEVELS.forEach(lvl => {
+      primaryLogger[lvl] = (...args) => {
+        primaryLogger.emit(lvl, ...args)
+      }
+
+      if (!ignoreNextLevels) {
+        primaryLogger.on(lvl, (msg) => {
+          let formatted = ''
+          if (WIKI.config.logFormat === 'json') {
+            formatted = JSON.stringify({
+              timestamp: new Date().toISOString(),
+              instance: WIKI.INSTANCE_ID,
+              level: lvl,
+              message: msg
+            })
+          } else {
+            if (msg instanceof Error) {
+              msg = msg.stack
+            }
+            formatted = `${new Date().toISOString()} ${chalk.dim('[' + WIKI.INSTANCE_ID + ']')} ${chalk[LEVELCOLORS[lvl]].bold(lvl)}: ${msg}`
+          }
+
+          console.log(formatted)
+          primaryLogger.ws.emit('log', formatted)
+        })
+      }
+      if (lvl === WIKI.config.logLevel) {
+        ignoreNextLevels = true
+      }
+    })
+
+    LEVELSIGNORED.forEach(lvl => {
+      primaryLogger[lvl] = () => {}
+    })
+
+    return primaryLogger
+  }
+}

+ 12 - 12
server/core/mail.js → server/core/mail.mjs

@@ -1,13 +1,13 @@
-const nodemailer = require('nodemailer')
-const _ = require('lodash')
-const fs = require('fs-extra')
-const path = require('path')
+import nodemailer from 'nodemailer'
+import { get, has, kebabCase, set, template } from 'lodash-es'
+import fs from 'node:fs/promises'
+import path from 'node:path'
 
-module.exports = {
+export default {
   transport: null,
   templates: {},
   init() {
-    if (_.get(WIKI.config, 'mail.host', '').length > 2) {
+    if (get(WIKI.config, 'mail.host', '').length > 2) {
       let conf = {
         host: WIKI.config.mail.host,
         port: WIKI.config.mail.port,
@@ -17,7 +17,7 @@ module.exports = {
           rejectUnauthorized: !(WIKI.config.mail.verifySSL === false)
         }
       }
-      if (_.get(WIKI.config, 'mail.user', '').length > 1) {
+      if (get(WIKI.config, 'mail.user', '').length > 1) {
         conf = {
           ...conf,
           auth: {
@@ -26,7 +26,7 @@ module.exports = {
           }
         }
       }
-      if (_.get(WIKI.config, 'mail.useDKIM', false)) {
+      if (get(WIKI.config, 'mail.useDKIM', false)) {
         conf = {
           ...conf,
           dkim: {
@@ -57,7 +57,7 @@ module.exports = {
       to: opts.to,
       subject: `${opts.subject} - ${WIKI.config.title}`,
       text: opts.text,
-      html: _.get(this.templates, opts.template)({
+      html: get(this.templates, opts.template)({
         logo: (WIKI.config.logoUrl.startsWith('http') ? '' : WIKI.config.host) + WIKI.config.logoUrl,
         siteTitle: WIKI.config.title,
         copyright: WIKI.config.company.length > 0 ? WIKI.config.company : 'Powered by Wiki.js',
@@ -66,11 +66,11 @@ module.exports = {
     })
   },
   async loadTemplate(key) {
-    if (_.has(this.templates, key)) { return }
-    const keyKebab = _.kebabCase(key)
+    if (has(this.templates, key)) { return }
+    const keyKebab = kebabCase(key)
     try {
       const rawTmpl = await fs.readFile(path.join(WIKI.SERVERPATH, `templates/${keyKebab}.html`), 'utf8')
-      _.set(this.templates, key, _.template(rawTmpl))
+      set(this.templates, key, template(rawTmpl))
     } catch (err) {
       WIKI.logger.warn(err)
       throw new WIKI.Error.MailTemplateFailed()

+ 17 - 13
server/core/scheduler.js → server/core/scheduler.mjs

@@ -1,14 +1,14 @@
-const { DynamicThreadPool } = require('poolifier')
-const os = require('node:os')
-const autoload = require('auto-load')
-const path = require('node:path')
-const cronparser = require('cron-parser')
-const { DateTime } = require('luxon')
-const { v4: uuid } = require('uuid')
-const { createDeferred } = require('../helpers/common')
-const _ = require('lodash')
+import { DynamicThreadPool } from 'poolifier'
+import os from 'node:os'
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import cronparser from 'cron-parser'
+import { DateTime } from 'luxon'
+import { v4 as uuid } from 'uuid'
+import { createDeferred } from '../helpers/common.mjs'
+import { find, remove } from 'lodash-es'
 
-module.exports = {
+export default {
   workerPool: null,
   maxWorkers: 1,
   activeWorkers: 0,
@@ -25,7 +25,11 @@ module.exports = {
       exitHandler: () => WIKI.logger.debug('A worker has gone offline.'),
       onlineHandler: () => WIKI.logger.debug('New worker is online.')
     })
-    this.tasks = autoload(path.join(WIKI.SERVERPATH, 'tasks/simple'))
+    this.tasks = {}
+    for (const f of (await fs.readdir(path.join(WIKI.SERVERPATH, 'tasks/simple')))) {
+      const taskName = f.replace('.mjs', '')
+      this.tasks[taskName] = (await import(path.join(WIKI.SERVERPATH, 'tasks/simple', f))).task
+    }
     return this
   },
   async start () {
@@ -43,7 +47,7 @@ module.exports = {
           break
         }
         case 'jobCompleted': {
-          const jobPromise = _.find(this.completionPromises, ['id', payload.id])
+          const jobPromise = find(this.completionPromises, ['id', payload.id])
           if (jobPromise) {
             if (payload.state === 'success') {
               jobPromise.resolve()
@@ -51,7 +55,7 @@ module.exports = {
               jobPromise.reject(new Error(payload.errorMessage))
             }
             setTimeout(() => {
-              _.remove(this.completionPromises, ['id', payload.id])
+              remove(this.completionPromises, ['id', payload.id])
             })
           }
           break

+ 19 - 17
server/core/servers.js → server/core/servers.mjs

@@ -1,13 +1,15 @@
-const fs = require('fs-extra')
-const http = require('http')
-const https = require('https')
-const { ApolloServer } = require('apollo-server-express')
-const _ = require('lodash')
-const io = require('socket.io')
-const { ApolloServerPluginLandingPageGraphQLPlayground, ApolloServerPluginLandingPageProductionDefault } = require('apollo-server-core')
-const { graphqlUploadExpress } = require('graphql-upload')
+import fs from 'node:fs/promises'
+import http from 'node:http'
+import https from 'node:https'
+import { ApolloServer } from 'apollo-server-express'
+import { isEmpty } from 'lodash-es'
+import { Server as IoServer } from 'socket.io'
+import { ApolloServerPluginLandingPageGraphQLPlayground, ApolloServerPluginLandingPageProductionDefault } from 'apollo-server-core'
+import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs'
 
-module.exports = {
+import { initSchema } from '../graph/index.mjs'
+
+export default {
   graph: null,
   http: null,
   https: null,
@@ -69,15 +71,15 @@ module.exports = {
     const tlsOpts = {}
     try {
       if (WIKI.config.ssl.format === 'pem') {
-        tlsOpts.key = WIKI.config.ssl.inline ? WIKI.config.ssl.key : fs.readFileSync(WIKI.config.ssl.key)
-        tlsOpts.cert = WIKI.config.ssl.inline ? WIKI.config.ssl.cert : fs.readFileSync(WIKI.config.ssl.cert)
+        tlsOpts.key = WIKI.config.ssl.inline ? WIKI.config.ssl.key : await fs.readFile(WIKI.config.ssl.key, 'utf-8')
+        tlsOpts.cert = WIKI.config.ssl.inline ? WIKI.config.ssl.cert : await fs.readFile(WIKI.config.ssl.cert, 'utf-8')
       } else {
-        tlsOpts.pfx = WIKI.config.ssl.inline ? WIKI.config.ssl.pfx : fs.readFileSync(WIKI.config.ssl.pfx)
+        tlsOpts.pfx = WIKI.config.ssl.inline ? WIKI.config.ssl.pfx : await fs.readFile(WIKI.config.ssl.pfx, 'utf-8')
       }
-      if (!_.isEmpty(WIKI.config.ssl.passphrase)) {
+      if (!isEmpty(WIKI.config.ssl.passphrase)) {
         tlsOpts.passphrase = WIKI.config.ssl.passphrase
       }
-      if (!_.isEmpty(WIKI.config.ssl.dhparam)) {
+      if (!isEmpty(WIKI.config.ssl.dhparam)) {
         tlsOpts.dhparam = WIKI.config.ssl.dhparam
       }
     } catch (err) {
@@ -127,7 +129,7 @@ module.exports = {
    * Start GraphQL Server
    */
   async startGraphQL () {
-    const graphqlSchema = require('../graph')
+    const graphqlSchema = await initSchema()
     this.graph = new ApolloServer({
       schema: graphqlSchema,
       csrfPrevention: true,
@@ -155,13 +157,13 @@ module.exports = {
    */
   async initWebSocket() {
     if (this.https) {
-      this.ws = new io.Server(this.https, {
+      this.ws = new IoServer(this.https, {
         path: '/_ws/',
         serveClient: false
       })
       WIKI.logger.info(`WebSocket Server attached to HTTPS Server [ OK ]`)
     } else {
-      this.ws = new io.Server(this.http, {
+      this.ws = new IoServer(this.http, {
         path: '/_ws/',
         serveClient: false,
         cors: true // TODO: dev only, replace with app settings once stable

+ 0 - 21
server/core/system.js

@@ -1,21 +0,0 @@
-const fs = require('fs-extra')
-const path = require('path')
-
-module.exports = {
-  updates: {
-    channel: 'BETA',
-    version: WIKI.version,
-    releaseDate: WIKI.releaseDate,
-    minimumVersionRequired: '3.0.0-beta.0',
-    minimumNodeRequired: '18.0.0'
-  },
-  init () {
-    fs.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'assets'))
-    fs.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'))
-
-    // Clear content cache
-    fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache'))
-
-    return this
-  }
-}

+ 21 - 0
server/core/system.mjs

@@ -0,0 +1,21 @@
+import fse from 'fs-extra'
+import path from 'node:path'
+
+export default {
+  updates: {
+    channel: 'BETA',
+    version: WIKI.version,
+    releaseDate: WIKI.releaseDate,
+    minimumVersionRequired: '3.0.0-beta.0',
+    minimumNodeRequired: '18.0.0'
+  },
+  init () {
+    fse.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'assets'))
+    fse.ensureDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads'))
+
+    // Clear content cache
+    fse.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'cache'))
+
+    return this
+  }
+}

+ 7 - 7
server/db/migrations/3.0.0.js → server/db/migrations/3.0.0.mjs

@@ -1,10 +1,10 @@
-const { v4: uuid } = require('uuid')
-const bcrypt = require('bcryptjs-then')
-const crypto = require('crypto')
-const { DateTime } = require('luxon')
-const pem2jwk = require('pem-jwk').pem2jwk
+import { v4 as uuid } from 'uuid'
+import bcrypt from 'bcryptjs-then'
+import crypto from 'node:crypto'
+import { DateTime } from 'luxon'
+import { pem2jwk } from 'pem-jwk'
 
-exports.up = async knex => {
+export async function up (knex) {
   WIKI.logger.info('Running 3.0.0 database migration...')
 
   // =====================================
@@ -778,4 +778,4 @@ exports.up = async knex => {
   WIKI.logger.info('Completed 3.0.0 database migration.')
 }
 
-exports.down = knex => { }
+export function down (knex) { }

+ 0 - 27
server/db/migrator-source.js

@@ -1,27 +0,0 @@
-const path = require('path')
-const fs = require('fs-extra')
-const semver = require('semver')
-
-const baseMigrationPath = path.join(WIKI.SERVERPATH, 'db/migrations')
-
-module.exports = {
-  /**
-   * Gets the migration names
-   * @returns Promise<string[]>
-   */
-  async getMigrations() {
-    const migrationFiles = await fs.readdir(baseMigrationPath)
-    return migrationFiles.map(m => m.replace('.js', '')).sort(semver.compare).map(m => ({
-      file: m,
-      directory: baseMigrationPath
-    }))
-  },
-
-  getMigrationName(migration) {
-    return migration.file.indexOf('.js') >= 0 ? migration.file : `${migration.file}.js`
-  },
-
-  getMigration(migration) {
-    return require(path.join(baseMigrationPath, migration.file))
-  }
-}

+ 26 - 0
server/db/migrator-source.mjs

@@ -0,0 +1,26 @@
+import path from 'node:path'
+import fse from 'fs-extra'
+import semver from 'semver'
+
+export default {
+  /**
+   * Gets the migration names
+   * @returns Promise<string[]>
+   */
+  async getMigrations() {
+    const baseMigrationPath = path.join(WIKI.SERVERPATH, 'db/migrations')
+    const migrationFiles = await fse.readdir(baseMigrationPath)
+    return migrationFiles.map(m => m.replace('.mjs', '')).sort(semver.compare).map(m => ({
+      file: m,
+      directory: baseMigrationPath
+    }))
+  },
+
+  getMigrationName(migration) {
+    return migration.file.indexOf('.mjs') >= 0 ? migration.file : `${migration.file}.mjs`
+  },
+
+  async getMigration(migration) {
+    return import(path.join(WIKI.SERVERPATH, 'db/migrations', `${migration.file}.mjs`))
+  }
+}

+ 0 - 54
server/graph/index.js

@@ -1,54 +0,0 @@
-const _ = require('lodash')
-const fs = require('fs')
-const path = require('path')
-const autoload = require('auto-load')
-const { makeExecutableSchema } = require('@graphql-tools/schema')
-const { defaultKeyGenerator, rateLimitDirective } = require('graphql-rate-limit-directive')
-const { GraphQLUpload } = require('graphql-upload')
-
-// Rate Limiter
-
-const { rateLimitDirectiveTypeDefs, rateLimitDirectiveTransformer } = rateLimitDirective({
-  keyGenerator: (directiveArgs, source, args, context, info) => `${context.req.ip}:${defaultKeyGenerator(directiveArgs, source, args, context, info)}`
-})
-
-// Schemas
-
-WIKI.logger.info(`Loading GraphQL Schema...`)
-const typeDefs = [
-  rateLimitDirectiveTypeDefs
-]
-const schemas = fs.readdirSync(path.join(WIKI.SERVERPATH, 'graph/schemas'))
-schemas.forEach(schema => {
-  typeDefs.push(fs.readFileSync(path.join(WIKI.SERVERPATH, `graph/schemas/${schema}`), 'utf8'))
-})
-
-// Resolvers
-
-WIKI.logger.info(`Loading GraphQL Resolvers...`)
-let resolvers = {
-  Date: require('./scalars/date'),
-  JSON: require('./scalars/json'),
-  UUID: require('./scalars/uuid'),
-  Upload: GraphQLUpload
-}
-const resolversObj = _.values(autoload(path.join(WIKI.SERVERPATH, 'graph/resolvers')))
-resolversObj.forEach(resolver => {
-  _.merge(resolvers, resolver)
-})
-
-// Make executable schema
-
-WIKI.logger.info(`Compiling GraphQL Schema...`)
-let schema = makeExecutableSchema({
-  typeDefs,
-  resolvers
-})
-
-// Apply schema transforms
-
-schema = rateLimitDirectiveTransformer(schema)
-
-WIKI.logger.info(`GraphQL Schema: [ OK ]`)
-
-module.exports = schema

+ 61 - 0
server/graph/index.mjs

@@ -0,0 +1,61 @@
+import { merge } from 'lodash-es'
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import { makeExecutableSchema } from '@graphql-tools/schema'
+import { defaultKeyGenerator, rateLimitDirective } from 'graphql-rate-limit-directive'
+import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'
+
+import DateScalar from './scalars/date.mjs'
+import JSONScalar from './scalars/json.mjs'
+import UUIDScalar from './scalars/uuid.mjs'
+
+export async function initSchema () {
+  // Rate Limiter
+
+  const { rateLimitDirectiveTypeDefs, rateLimitDirectiveTransformer } = rateLimitDirective({
+    keyGenerator: (directiveArgs, source, args, context, info) => `${context.req.ip}:${defaultKeyGenerator(directiveArgs, source, args, context, info)}`
+  })
+
+  // Schemas
+
+  WIKI.logger.info(`Loading GraphQL Schema...`)
+  const typeDefs = [
+    rateLimitDirectiveTypeDefs
+  ]
+  const schemaList = await fs.readdir(path.join(WIKI.SERVERPATH, 'graph/schemas'))
+  for (const schemaFile of schemaList) {
+    typeDefs.push(await fs.readFile(path.join(WIKI.SERVERPATH, `graph/schemas/${schemaFile}`), 'utf8'))
+  }
+
+  // Resolvers
+
+  WIKI.logger.info(`Loading GraphQL Resolvers...`)
+  let resolvers = {
+    Date: DateScalar,
+    JSON: JSONScalar,
+    UUID: UUIDScalar,
+    Upload: GraphQLUpload
+  }
+
+  const resolverList = await fs.readdir(path.join(WIKI.SERVERPATH, 'graph/resolvers'))
+  for (const resolverFile of resolverList) {
+    const resolver = (await import(path.join(WIKI.SERVERPATH, 'graph/resolvers', resolverFile))).default
+    merge(resolvers, resolver)
+  }
+
+  // Make executable schema
+
+  WIKI.logger.info(`Compiling GraphQL Schema...`)
+  let schema = makeExecutableSchema({
+    typeDefs,
+    resolvers
+  })
+
+  // Apply schema transforms
+
+  schema = rateLimitDirectiveTransformer(schema)
+
+  WIKI.logger.info(`GraphQL Schema: [ OK ]`)
+
+  return schema
+}

+ 10 - 10
server/graph/resolvers/analytics.js → server/graph/resolvers/analytics.mjs

@@ -1,17 +1,17 @@
-const _ = require('lodash')
-const graphHelper = require('../../helpers/graph')
+import { find, get, reduce, set, sortBy, transform } from 'lodash-es'
+import { generateError, generateSuccess } from '../../helpers/graph.mjs'
 
-module.exports = {
+export default {
   Query: {
     async analyticsProviders(obj, args, context, info) {
       let providers = await WIKI.db.analytics.getProviders(args.isEnabled)
       providers = providers.map(stg => {
-        const providerInfo = _.find(WIKI.data.analytics, ['key', stg.key]) || {}
+        const providerInfo = find(WIKI.data.analytics, ['key', stg.key]) || {}
         return {
           ...providerInfo,
           ...stg,
-          config: _.sortBy(_.transform(stg.config, (res, value, key) => {
-            const configData = _.get(providerInfo.props, key, {})
+          config: sortBy(transform(stg.config, (res, value, key) => {
+            const configData = get(providerInfo.props, key, {})
             res.push({
               key,
               value: JSON.stringify({
@@ -31,18 +31,18 @@ module.exports = {
         for (let str of args.providers) {
           await WIKI.db.analytics.query().patch({
             isEnabled: str.isEnabled,
-            config: _.reduce(str.config, (result, value, key) => {
-              _.set(result, `${value.key}`, _.get(JSON.parse(value.value), 'v', null))
+            config: reduce(str.config, (result, value, key) => {
+              set(result, `${value.key}`, get(JSON.parse(value.value), 'v', null))
               return result
             }, {})
           }).where('key', str.key)
           await WIKI.cache.del('analytics')
         }
         return {
-          responseResult: graphHelper.generateSuccess('Providers updated successfully')
+          responseResult: generateSuccess('Providers updated successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     }
   }

+ 17 - 17
server/graph/resolvers/asset.js → server/graph/resolvers/asset.mjs

@@ -1,12 +1,12 @@
-const _ = require('lodash')
-const sanitize = require('sanitize-filename')
-const graphHelper = require('../../helpers/graph')
-const path = require('node:path')
-const fs = require('fs-extra')
-const { v4: uuid } = require('uuid')
-const { pipeline } = require('node:stream/promises')
-
-module.exports = {
+import _ from 'lodash-es'
+import sanitize from 'sanitize-filename'
+import { generateError, generateSuccess } from '../../helpers/graph.mjs'
+import path from 'node:path'
+import fs from 'fs-extra'
+import { v4 as uuid } from 'uuid'
+import { pipeline } from 'node:stream/promises'
+
+export default {
   Query: {
     async assetById(obj, args, context) {
       return null
@@ -83,13 +83,13 @@ module.exports = {
           })
 
           return {
-            responseResult: graphHelper.generateSuccess('Asset has been renamed successfully.')
+            responseResult: generateSuccess('Asset has been renamed successfully.')
           }
         } else {
           throw new WIKI.Error.AssetInvalid()
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -122,13 +122,13 @@ module.exports = {
           })
 
           return {
-            responseResult: graphHelper.generateSuccess('Asset has been deleted successfully.')
+            responseResult: generateSuccess('Asset has been deleted successfully.')
           }
         } else {
           throw new WIKI.Error.AssetInvalid()
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -357,13 +357,13 @@ module.exports = {
           } else {
             WIKI.logger.debug('Asset(s) uploaded successfully.')
             return {
-              operation: graphHelper.generateSuccess('Asset(s) uploaded successfully')
+              operation: generateSuccess('Asset(s) uploaded successfully')
             }
           }
         }
       } catch (err) {
         WIKI.logger.warn(err)
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -373,10 +373,10 @@ module.exports = {
       try {
         await WIKI.db.assets.flushTempUploads()
         return {
-          responseResult: graphHelper.generateSuccess('Temporary Uploads have been flushed successfully.')
+          responseResult: generateSuccess('Temporary Uploads have been flushed successfully.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     }
   }

+ 25 - 25
server/graph/resolvers/authentication.js → server/graph/resolvers/authentication.mjs

@@ -1,7 +1,7 @@
-const _ = require('lodash')
-const graphHelper = require('../../helpers/graph')
+import _ from 'lodash-es'
+import { generateError, generateSuccess } from '../../helpers/graph.mjs'
 
-module.exports = {
+export default {
   Query: {
     /**
      * List of API Keys
@@ -67,11 +67,11 @@ module.exports = {
         WIKI.events.outbound.emit('reloadApiKeys')
         return {
           key,
-          operation: graphHelper.generateSuccess('API Key created successfully')
+          operation: generateSuccess('API Key created successfully')
         }
       } catch (err) {
         WIKI.logger.warn(err)
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -82,7 +82,7 @@ module.exports = {
         const authResult = await WIKI.db.users.login(args, context)
         return {
           ...authResult,
-          operation: graphHelper.generateSuccess('Login success')
+          operation: generateSuccess('Login success')
         }
       } catch (err) {
         // LDAP Debug Flag
@@ -91,7 +91,7 @@ module.exports = {
         }
         console.error(err)
 
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -102,10 +102,10 @@ module.exports = {
         const authResult = await WIKI.db.users.loginTFA(args, context)
         return {
           ...authResult,
-          responseResult: graphHelper.generateSuccess('TFA success')
+          responseResult: generateSuccess('TFA success')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -116,10 +116,10 @@ module.exports = {
         const authResult = await WIKI.db.users.loginChangePassword(args, context)
         return {
           ...authResult,
-          responseResult: graphHelper.generateSuccess('Password changed successfully')
+          responseResult: generateSuccess('Password changed successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -129,10 +129,10 @@ module.exports = {
       try {
         await WIKI.db.users.loginForgotPassword(args, context)
         return {
-          responseResult: graphHelper.generateSuccess('Password reset request processed.')
+          responseResult: generateSuccess('Password reset request processed.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -142,10 +142,10 @@ module.exports = {
       try {
         await WIKI.db.users.register({ ...args, verify: true }, context)
         return {
-          responseResult: graphHelper.generateSuccess('Registration success')
+          responseResult: generateSuccess('Registration success')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -156,10 +156,10 @@ module.exports = {
         WIKI.config.api.isEnabled = args.enabled
         await WIKI.configSvc.saveToDb(['api'])
         return {
-          operation: graphHelper.generateSuccess('API State changed successfully')
+          operation: generateSuccess('API State changed successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -173,10 +173,10 @@ module.exports = {
         await WIKI.auth.reloadApiKeys()
         WIKI.events.outbound.emit('reloadApiKeys')
         return {
-          operation: graphHelper.generateSuccess('API Key revoked successfully')
+          operation: generateSuccess('API Key revoked successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -226,10 +226,10 @@ module.exports = {
         await WIKI.auth.activateStrategies()
         WIKI.events.outbound.emit('reloadAuthStrategies')
         return {
-          responseResult: graphHelper.generateSuccess('Strategies updated successfully')
+          responseResult: generateSuccess('Strategies updated successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -239,10 +239,10 @@ module.exports = {
       try {
         await WIKI.auth.regenerateCertificates()
         return {
-          responseResult: graphHelper.generateSuccess('Certificates have been regenerated successfully.')
+          responseResult: generateSuccess('Certificates have been regenerated successfully.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -252,10 +252,10 @@ module.exports = {
       try {
         await WIKI.auth.resetGuestUser()
         return {
-          responseResult: graphHelper.generateSuccess('Guest user has been reset successfully.')
+          responseResult: generateSuccess('Guest user has been reset successfully.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     }
   },

+ 11 - 11
server/graph/resolvers/comment.js → server/graph/resolvers/comment.mjs

@@ -1,7 +1,7 @@
-const _ = require('lodash')
-const graphHelper = require('../../helpers/graph')
+import _ from 'lodash-es'
+import { generateError, generateSuccess } from '../../helpers/graph.mjs'
 
-module.exports = {
+export default {
   Query: {
     /**
      * Fetch list of Comments Providers
@@ -90,11 +90,11 @@ module.exports = {
           ip: context.req.ip
         })
         return {
-          responseResult: graphHelper.generateSuccess('New comment posted successfully'),
+          responseResult: generateSuccess('New comment posted successfully'),
           id: cmId
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -108,11 +108,11 @@ module.exports = {
           ip: context.req.ip
         })
         return {
-          responseResult: graphHelper.generateSuccess('Comment updated successfully'),
+          responseResult: generateSuccess('Comment updated successfully'),
           render: cmRender
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -126,10 +126,10 @@ module.exports = {
           ip: context.req.ip
         })
         return {
-          responseResult: graphHelper.generateSuccess('Comment deleted successfully')
+          responseResult: generateSuccess('Comment deleted successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -148,10 +148,10 @@ module.exports = {
         }
         await WIKI.db.commentProviders.initProvider()
         return {
-          responseResult: graphHelper.generateSuccess('Comment Providers updated successfully')
+          responseResult: generateSuccess('Comment Providers updated successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     }
   }

+ 10 - 10
server/graph/resolvers/group.js → server/graph/resolvers/group.mjs

@@ -1,9 +1,9 @@
-const graphHelper = require('../../helpers/graph')
-const safeRegex = require('safe-regex')
-const _ = require('lodash')
-const { v4: uuid } = require('uuid')
+import { generateError, generateSuccess } from '../../helpers/graph.mjs'
+import safeRegex from 'safe-regex'
+import _ from 'lodash-es'
+import { v4 as uuid } from 'uuid'
 
-module.exports = {
+export default {
   Query: {
     /**
      * FETCH ALL GROUPS
@@ -71,7 +71,7 @@ module.exports = {
       WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' })
 
       return {
-        operation: graphHelper.generateSuccess('User has been assigned to group.')
+        operation: generateSuccess('User has been assigned to group.')
       }
     },
     /**
@@ -90,7 +90,7 @@ module.exports = {
       await WIKI.auth.reloadGroups()
       WIKI.events.outbound.emit('reloadGroups')
       return {
-        operation: graphHelper.generateSuccess('Group created successfully.'),
+        operation: generateSuccess('Group created successfully.'),
         group
       }
     },
@@ -111,7 +111,7 @@ module.exports = {
       WIKI.events.outbound.emit('reloadGroups')
 
       return {
-        operation: graphHelper.generateSuccess('Group has been deleted.')
+        operation: generateSuccess('Group has been deleted.')
       }
     },
     /**
@@ -138,7 +138,7 @@ module.exports = {
       WIKI.events.outbound.emit('addAuthRevoke', { id: usr.id, kind: 'u' })
 
       return {
-        operation: graphHelper.generateSuccess('User has been unassigned from group.')
+        operation: generateSuccess('User has been unassigned from group.')
       }
     },
     /**
@@ -193,7 +193,7 @@ module.exports = {
       WIKI.events.outbound.emit('reloadGroups')
 
       return {
-        operation: graphHelper.generateSuccess('Group has been updated.')
+        operation: generateSuccess('Group has been updated.')
       }
     }
   },

+ 9 - 9
server/graph/resolvers/hooks.js → server/graph/resolvers/hooks.mjs

@@ -1,7 +1,7 @@
-const graphHelper = require('../../helpers/graph')
-const _ = require('lodash')
+import { generateError, generateSuccess } from '../../helpers/graph.mjs'
+import _ from 'lodash-es'
 
-module.exports = {
+export default {
   Query: {
     async hooks () {
       return WIKI.db.hooks.query().orderBy('name')
@@ -31,11 +31,11 @@ module.exports = {
         WIKI.logger.debug(`New Hook ${newHook.id} created successfully.`)
 
         return {
-          operation: graphHelper.generateSuccess('Hook created successfully'),
+          operation: generateSuccess('Hook created successfully'),
           hook: newHook
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -63,10 +63,10 @@ module.exports = {
         WIKI.logger.debug(`Hook ${args.id} updated successfully.`)
 
         return {
-          operation: graphHelper.generateSuccess('Hook updated successfully')
+          operation: generateSuccess('Hook updated successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -77,10 +77,10 @@ module.exports = {
         await WIKI.db.hooks.deleteHook(args.id)
         WIKI.logger.debug(`Hook ${args.id} deleted successfully.`)
         return {
-          operation: graphHelper.generateSuccess('Hook deleted successfully')
+          operation: generateSuccess('Hook deleted successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     }
   }

+ 7 - 7
server/graph/resolvers/localization.js → server/graph/resolvers/localization.mjs

@@ -1,7 +1,7 @@
-const graphHelper = require('../../helpers/graph')
-const _ = require('lodash')
+import { generateError, generateSuccess } from '../../helpers/graph.mjs'
+import _ from 'lodash-es'
 
-module.exports = {
+export default {
   Query: {
     async locales(obj, args, context, info) {
       let remoteLocales = await WIKI.cache.get('locales')
@@ -29,10 +29,10 @@ module.exports = {
         }, args.locale)
         await job.finished
         return {
-          responseResult: graphHelper.generateSuccess('Locale downloaded successfully')
+          responseResult: generateSuccess('Locale downloaded successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     async updateLocale(obj, args, context) {
@@ -53,10 +53,10 @@ module.exports = {
         await WIKI.cache.del('nav:locales')
 
         return {
-          responseResult: graphHelper.generateSuccess('Locale config updated')
+          responseResult: generateSuccess('Locale config updated')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     }
   }

+ 7 - 7
server/graph/resolvers/mail.js → server/graph/resolvers/mail.mjs

@@ -1,7 +1,7 @@
-const _ = require('lodash')
-const graphHelper = require('../../helpers/graph')
+import _ from 'lodash-es'
+import { generateError, generateSuccess } from '../../helpers/graph.mjs'
 
-module.exports = {
+export default {
   Query: {
     async mailConfig(obj, args, context, info) {
       return {
@@ -28,10 +28,10 @@ module.exports = {
         })
 
         return {
-          operation: graphHelper.generateSuccess('Test email sent successfully.')
+          operation: generateSuccess('Test email sent successfully.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     async updateMailConfig(obj, args, context) {
@@ -56,10 +56,10 @@ module.exports = {
         WIKI.mail.init()
 
         return {
-          operation: graphHelper.generateSuccess('Mail configuration updated successfully.')
+          operation: generateSuccess('Mail configuration updated successfully.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     }
   }

+ 6 - 6
server/graph/resolvers/navigation.js → server/graph/resolvers/navigation.mjs

@@ -1,6 +1,6 @@
-const graphHelper = require('../../helpers/graph')
+import { generateError, generateSuccess } from '../../helpers/graph.mjs'
 
-module.exports = {
+export default {
   Query: {
     async navigationTree (obj, args, context, info) {
       return WIKI.db.navigation.getTree({ cache: false, locale: 'all', bypassAuth: true })
@@ -20,10 +20,10 @@ module.exports = {
         }
 
         return {
-          responseResult: graphHelper.generateSuccess('Navigation updated successfully')
+          responseResult: generateSuccess('Navigation updated successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     async updateNavigationConfig (obj, args, context) {
@@ -34,10 +34,10 @@ module.exports = {
         await WIKI.configSvc.saveToDb(['nav'])
 
         return {
-          responseResult: graphHelper.generateSuccess('Navigation config updated successfully')
+          responseResult: generateSuccess('Navigation config updated successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     }
   }

+ 31 - 31
server/graph/resolvers/page.js → server/graph/resolvers/page.mjs

@@ -1,8 +1,8 @@
-const _ = require('lodash')
-const graphHelper = require('../../helpers/graph')
-const pageHelper = require('../../helpers/page')
+import _ from 'lodash-es'
+import { generateError, generateSuccess } from '../../helpers/graph.mjs'
+import { parsePath }from '../../helpers/page.mjs'
 
-module.exports = {
+export default {
   Query: {
     /**
      * PAGE HISTORY
@@ -169,7 +169,7 @@ module.exports = {
      */
     async pageByPath (obj, args, context, info) {
       // console.info(info)
-      const pageArgs = pageHelper.parsePath(args.path)
+      const pageArgs = parsePath(args.path)
       const page = await WIKI.db.pages.getPageFromDb({
         ...pageArgs,
         siteId: args.siteId
@@ -393,11 +393,11 @@ module.exports = {
           user: context.req.user
         })
         return {
-          operation: graphHelper.generateSuccess('Page created successfully.'),
+          operation: generateSuccess('Page created successfully.'),
           page
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -410,11 +410,11 @@ module.exports = {
           user: context.req.user
         })
         return {
-          operation: graphHelper.generateSuccess('Page has been updated.'),
+          operation: generateSuccess('Page has been updated.'),
           page
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -427,10 +427,10 @@ module.exports = {
           user: context.req.user
         })
         return {
-          responseResult: graphHelper.generateSuccess('Page has been converted.')
+          responseResult: generateSuccess('Page has been converted.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -443,10 +443,10 @@ module.exports = {
           user: context.req.user
         })
         return {
-          responseResult: graphHelper.generateSuccess('Page has been moved.')
+          responseResult: generateSuccess('Page has been moved.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -459,10 +459,10 @@ module.exports = {
           user: context.req.user
         })
         return {
-          responseResult: graphHelper.generateSuccess('Page has been deleted.')
+          responseResult: generateSuccess('Page has been deleted.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -478,10 +478,10 @@ module.exports = {
           throw new Error('This tag does not exist.')
         }
         return {
-          responseResult: graphHelper.generateSuccess('Tag has been deleted.')
+          responseResult: generateSuccess('Tag has been deleted.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -499,10 +499,10 @@ module.exports = {
           throw new Error('This tag does not exist.')
         }
         return {
-          responseResult: graphHelper.generateSuccess('Tag has been updated successfully.')
+          responseResult: generateSuccess('Tag has been updated successfully.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -513,10 +513,10 @@ module.exports = {
         await WIKI.db.pages.flushCache()
         WIKI.events.outbound.emit('flushCache')
         return {
-          responseResult: graphHelper.generateSuccess('Pages Cache has been flushed successfully.')
+          responseResult: generateSuccess('Pages Cache has been flushed successfully.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -526,11 +526,11 @@ module.exports = {
       try {
         const count = await WIKI.db.pages.migrateToLocale(args)
         return {
-          responseResult: graphHelper.generateSuccess('Migrated content to target locale successfully.'),
+          responseResult: generateSuccess('Migrated content to target locale successfully.'),
           count
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -540,10 +540,10 @@ module.exports = {
       try {
         await WIKI.db.pages.rebuildTree()
         return {
-          responseResult: graphHelper.generateSuccess('Page tree rebuilt successfully.')
+          responseResult: generateSuccess('Page tree rebuilt successfully.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -557,10 +557,10 @@ module.exports = {
         }
         await WIKI.db.pages.renderPage(page)
         return {
-          responseResult: graphHelper.generateSuccess('Page rendered successfully.')
+          responseResult: generateSuccess('Page rendered successfully.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -593,10 +593,10 @@ module.exports = {
         })
 
         return {
-          responseResult: graphHelper.generateSuccess('Page version restored successfully.')
+          responseResult: generateSuccess('Page version restored successfully.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -606,10 +606,10 @@ module.exports = {
       try {
         await WIKI.db.pageHistory.purge(args.olderThan)
         return {
-          responseResult: graphHelper.generateSuccess('Page history purged successfully.')
+          responseResult: generateSuccess('Page history purged successfully.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     }
   },

+ 5 - 5
server/graph/resolvers/rendering.js → server/graph/resolvers/rendering.mjs

@@ -1,7 +1,7 @@
-const _ = require('lodash')
-const graphHelper = require('../../helpers/graph')
+import _ from 'lodash-es'
+import { generateError, generateSuccess } from '../../helpers/graph.mjs'
 
-module.exports = {
+export default {
   Query: {
     async renderers(obj, args, context, info) {
       let renderers = await WIKI.db.renderers.getRenderers()
@@ -42,10 +42,10 @@ module.exports = {
           }).where('key', rdr.key)
         }
         return {
-          responseResult: graphHelper.generateSuccess('Renderers updated successfully')
+          responseResult: generateSuccess('Renderers updated successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     }
   }

+ 0 - 16
server/graph/resolvers/search.js

@@ -1,16 +0,0 @@
-const graphHelper = require('../../helpers/graph')
-
-module.exports = {
-  Mutation: {
-    async rebuildSearchIndex (obj, args, context) {
-      try {
-        await WIKI.data.searchEngine.rebuild()
-        return {
-          responseResult: graphHelper.generateSuccess('Index rebuilt successfully')
-        }
-      } catch (err) {
-        return graphHelper.generateError(err)
-      }
-    }
-  }
-}

+ 16 - 0
server/graph/resolvers/search.mjs

@@ -0,0 +1,16 @@
+import { generateError, generateSuccess } from '../../helpers/graph.mjs'
+
+export default {
+  Mutation: {
+    async rebuildSearchIndex (obj, args, context) {
+      try {
+        await WIKI.data.searchEngine.rebuild()
+        return {
+          responseResult: generateSuccess('Index rebuilt successfully')
+        }
+      } catch (err) {
+        return generateError(err)
+      }
+    }
+  }
+}

+ 19 - 19
server/graph/resolvers/site.js → server/graph/resolvers/site.mjs

@@ -1,11 +1,11 @@
-const graphHelper = require('../../helpers/graph')
-const _ = require('lodash')
-const CleanCSS = require('clean-css')
-const path = require('path')
-const fs = require('fs-extra')
-const { v4: uuid } = require('uuid')
+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'
 
-module.exports = {
+export default {
   Query: {
     async sites () {
       const sites = await WIKI.db.sites.query().orderBy('hostname')
@@ -72,12 +72,12 @@ module.exports = {
           title: args.title
         })
         return {
-          operation: graphHelper.generateSuccess('Site created successfully'),
+          operation: generateSuccess('Site created successfully'),
           site: newSite
         }
       } catch (err) {
         WIKI.logger.warn(err)
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -117,11 +117,11 @@ module.exports = {
         })
 
         return {
-          operation: graphHelper.generateSuccess('Site updated successfully')
+          operation: generateSuccess('Site updated successfully')
         }
       } catch (err) {
         WIKI.logger.warn(err)
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -137,11 +137,11 @@ module.exports = {
         // -> Delete site
         await WIKI.db.sites.deleteSite(args.id)
         return {
-          operation: graphHelper.generateSuccess('Site deleted successfully')
+          operation: generateSuccess('Site deleted successfully')
         }
       } catch (err) {
         WIKI.logger.warn(err)
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -197,11 +197,11 @@ module.exports = {
         }).onConflict('id').merge()
         WIKI.logger.info('New site logo processed successfully.')
         return {
-          operation: graphHelper.generateSuccess('Site logo uploaded successfully')
+          operation: generateSuccess('Site logo uploaded successfully')
         }
       } catch (err) {
         WIKI.logger.warn(err)
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -258,11 +258,11 @@ module.exports = {
         }).onConflict('id').merge()
         WIKI.logger.info('New site favicon processed successfully.')
         return {
-          operation: graphHelper.generateSuccess('Site favicon uploaded successfully')
+          operation: generateSuccess('Site favicon uploaded successfully')
         }
       } catch (err) {
         WIKI.logger.warn(err)
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -316,11 +316,11 @@ module.exports = {
         }).onConflict('id').merge()
         WIKI.logger.info('New site login bg processed successfully.')
         return {
-          operation: graphHelper.generateSuccess('Site login bg uploaded successfully')
+          operation: generateSuccess('Site login bg uploaded successfully')
         }
       } catch (err) {
         WIKI.logger.warn(err)
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     }
   }

+ 12 - 12
server/graph/resolvers/storage.js → server/graph/resolvers/storage.mjs

@@ -1,8 +1,8 @@
-const _ = require('lodash')
-const graphHelper = require('../../helpers/graph')
-const { v4: uuid } = require('uuid')
+import _ from 'lodash-es'
+import { generateError, generateSuccess } from '../../helpers/graph.mjs'
+import { v4 as uuid } from 'uuid'
 
-module.exports = {
+export default {
   Query: {
     async storageTargets (obj, args, context, info) {
       const dbTargets = await WIKI.db.storage.getTargets({ siteId: args.siteId })
@@ -164,10 +164,10 @@ module.exports = {
         }
         // await WIKI.db.storage.initTargets()
         return {
-          status: graphHelper.generateSuccess('Storage targets updated successfully')
+          status: generateSuccess('Storage targets updated successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     async setupStorageTarget (obj, args, context) {
@@ -186,11 +186,11 @@ module.exports = {
         const result = await WIKI.storage.modules[md.key].setup(args.targetId, args.state)
 
         return {
-          status: graphHelper.generateSuccess('Storage target setup step succeeded'),
+          status: generateSuccess('Storage target setup step succeeded'),
           state: result
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     async destroyStorageTargetSetup (obj, args, context) {
@@ -209,20 +209,20 @@ module.exports = {
         await WIKI.storage.modules[md.key].setupDestroy(args.targetId)
 
         return {
-          status: graphHelper.generateSuccess('Storage target setup configuration destroyed succesfully.')
+          status: generateSuccess('Storage target setup configuration destroyed succesfully.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     async executeStorageAction (obj, args, context) {
       try {
         await WIKI.db.storage.executeAction(args.targetKey, args.handler)
         return {
-          status: graphHelper.generateSuccess('Action completed.')
+          status: generateSuccess('Action completed.')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     }
   }

+ 21 - 19
server/graph/resolvers/system.js → server/graph/resolvers/system.mjs

@@ -1,14 +1,16 @@
-const _ = require('lodash')
-const util = require('node:util')
-const getos = util.promisify(require('getos'))
-const os = require('node:os')
-const filesize = require('filesize')
-const path = require('path')
-const fs = require('fs-extra')
-const { DateTime } = require('luxon')
-const graphHelper = require('../../helpers/graph')
+import _ from 'lodash-es'
+import util from 'node:util'
+import getosSync from 'getos'
+import os from 'node:os'
+import filesize from 'filesize'
+import path from 'node:path'
+import fs from 'fs-extra'
+import { DateTime } from 'luxon'
+import { generateError, generateSuccess } from '../../helpers/graph.mjs'
 
-module.exports = {
+const getos = util.promisify(getosSync)
+
+export default {
   Query: {
     systemFlags () {
       return WIKI.config.flags
@@ -89,18 +91,18 @@ module.exports = {
           throw new Error('Job has already entered active state or does not exist.')
         }
         return {
-          operation: graphHelper.generateSuccess('Cancelled job successfully.')
+          operation: generateSuccess('Cancelled job successfully.')
         }
       } catch (err) {
         WIKI.logger.warn(err)
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     async disconnectWS (obj, args, context) {
       WIKI.servers.ws.disconnectSockets(true)
       WIKI.logger.info('All active websocket connections have been terminated.')
       return {
-        operation: graphHelper.generateSuccess('All websocket connections closed successfully.')
+        operation: generateSuccess('All websocket connections closed successfully.')
       }
     },
     async installExtension (obj, args, context) {
@@ -108,10 +110,10 @@ module.exports = {
         await WIKI.extensions.ext[args.key].install()
         // TODO: broadcast ext install
         return {
-          operation: graphHelper.generateSuccess('Extension installed successfully')
+          operation: generateSuccess('Extension installed successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     async retryJob (obj, args, context) {
@@ -140,11 +142,11 @@ module.exports = {
           })
         WIKI.logger.info(`Job ${args.id} has been rescheduled [ OK ]`)
         return {
-          operation: graphHelper.generateSuccess('Job rescheduled successfully.')
+          operation: generateSuccess('Job rescheduled successfully.')
         }
       } catch (err) {
         WIKI.logger.warn(err)
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     async updateSystemFlags (obj, args, context) {
@@ -155,7 +157,7 @@ module.exports = {
       await WIKI.configSvc.applyFlags()
       await WIKI.configSvc.saveToDb(['flags'])
       return {
-        operation: graphHelper.generateSuccess('System Flags applied successfully')
+        operation: generateSuccess('System Flags applied successfully')
       }
     },
     async updateSystemSecurity (obj, args, context) {
@@ -163,7 +165,7 @@ module.exports = {
       // TODO: broadcast config update
       await WIKI.configSvc.saveToDb(['security'])
       return {
-        operation: graphHelper.generateSuccess('System Security configuration applied successfully')
+        operation: generateSuccess('System Security configuration applied successfully')
       }
     }
   },

+ 9 - 9
server/graph/resolvers/tree.js → server/graph/resolvers/tree.mjs

@@ -1,5 +1,5 @@
-const _ = require('lodash')
-const graphHelper = require('../../helpers/graph')
+import _ from 'lodash-es'
+import { generateError, generateSuccess } from '../../helpers/graph.mjs'
 
 const typeResolvers = {
   folder: 'TreeItemFolder',
@@ -10,7 +10,7 @@ const typeResolvers = {
 const rePathName = /^[a-z0-9-]+$/
 const reTitle = /^[^<>"]+$/
 
-module.exports = {
+export default {
   Query: {
     /**
      * FETCH TREE
@@ -162,11 +162,11 @@ module.exports = {
         await WIKI.db.tree.createFolder(args)
 
         return {
-          operation: graphHelper.generateSuccess('Folder created successfully')
+          operation: generateSuccess('Folder created successfully')
         }
       } catch (err) {
         WIKI.logger.debug(`Failed to create folder: ${err.message}`)
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -177,11 +177,11 @@ module.exports = {
         await WIKI.db.tree.renameFolder(args)
 
         return {
-          operation: graphHelper.generateSuccess('Folder renamed successfully')
+          operation: generateSuccess('Folder renamed successfully')
         }
       } catch (err) {
         WIKI.logger.debug(`Failed to rename folder ${args.folderId}: ${err.message}`)
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -192,11 +192,11 @@ module.exports = {
         await WIKI.db.tree.deleteFolder(args.folderId)
 
         return {
-          operation: graphHelper.generateSuccess('Folder deleted successfully')
+          operation: generateSuccess('Folder deleted successfully')
         }
       } catch (err) {
         WIKI.logger.debug(`Failed to delete folder ${args.folderId}: ${err.message}`)
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     }
   },

+ 30 - 30
server/graph/resolvers/user.js → server/graph/resolvers/user.mjs

@@ -1,9 +1,9 @@
-const graphHelper = require('../../helpers/graph')
-const _ = require('lodash')
-const path = require('node:path')
-const fs = require('fs-extra')
+import { generateError, generateSuccess } from '../../helpers/graph.mjs'
+import _ from 'lodash-es'
+import path from 'node:path'
+import fs from 'fs-extra'
 
-module.exports = {
+export default {
   Query: {
     /**
      * FETCH ALL USERS
@@ -93,10 +93,10 @@ module.exports = {
         await WIKI.db.users.createNewUser({ ...args, passwordRaw: args.password, isVerified: true })
 
         return {
-          operation: graphHelper.generateSuccess('User created successfully')
+          operation: generateSuccess('User created successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     async deleteUser (obj, args) {
@@ -110,13 +110,13 @@ module.exports = {
         WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })
 
         return {
-          operation: graphHelper.generateSuccess('User deleted successfully')
+          operation: generateSuccess('User deleted successfully')
         }
       } catch (err) {
         if (err.message.indexOf('foreign') >= 0) {
-          return graphHelper.generateError(new WIKI.Error.UserDeleteForeignConstraint())
+          return generateError(new WIKI.Error.UserDeleteForeignConstraint())
         } else {
-          return graphHelper.generateError(err)
+          return generateError(err)
         }
       }
     },
@@ -125,10 +125,10 @@ module.exports = {
         await WIKI.db.users.updateUser(args.id, args.patch)
 
         return {
-          operation: graphHelper.generateSuccess('User updated successfully')
+          operation: generateSuccess('User updated successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     async verifyUser (obj, args) {
@@ -136,10 +136,10 @@ module.exports = {
         await WIKI.db.users.query().patch({ isVerified: true }).findById(args.id)
 
         return {
-          operation: graphHelper.generateSuccess('User verified successfully')
+          operation: generateSuccess('User verified successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     async activateUser (obj, args) {
@@ -147,10 +147,10 @@ module.exports = {
         await WIKI.db.users.query().patch({ isActive: true }).findById(args.id)
 
         return {
-          operation: graphHelper.generateSuccess('User activated successfully')
+          operation: generateSuccess('User activated successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     async deactivateUser (obj, args) {
@@ -164,10 +164,10 @@ module.exports = {
         WIKI.events.outbound.emit('addAuthRevoke', { id: args.id, kind: 'u' })
 
         return {
-          operation: graphHelper.generateSuccess('User deactivated successfully')
+          operation: generateSuccess('User deactivated successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     async enableUserTFA (obj, args) {
@@ -175,10 +175,10 @@ module.exports = {
         await WIKI.db.users.query().patch({ tfaIsActive: true, tfaSecret: null }).findById(args.id)
 
         return {
-          operation: graphHelper.generateSuccess('User 2FA enabled successfully')
+          operation: generateSuccess('User 2FA enabled successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     async disableUserTFA (obj, args) {
@@ -186,10 +186,10 @@ module.exports = {
         await WIKI.db.users.query().patch({ tfaIsActive: false, tfaSecret: null }).findById(args.id)
 
         return {
-          operation: graphHelper.generateSuccess('User 2FA disabled successfully')
+          operation: generateSuccess('User 2FA disabled successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     resetUserPassword (obj, args) {
@@ -235,10 +235,10 @@ module.exports = {
         })
 
         return {
-          operation: graphHelper.generateSuccess('User profile updated successfully')
+          operation: generateSuccess('User profile updated successfully')
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     async changePassword (obj, args, context) {
@@ -270,11 +270,11 @@ module.exports = {
         const newToken = await WIKI.db.users.refreshToken(usr)
 
         return {
-          responseResult: graphHelper.generateSuccess('Password changed successfully'),
+          responseResult: generateSuccess('Password changed successfully'),
           jwt: newToken.token
         }
       } catch (err) {
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -316,11 +316,11 @@ module.exports = {
         }).onConflict('id').merge()
         WIKI.logger.debug(`Processed user ${args.id} avatar successfully.`)
         return {
-          operation: graphHelper.generateSuccess('User avatar uploaded successfully')
+          operation: generateSuccess('User avatar uploaded successfully')
         }
       } catch (err) {
         WIKI.logger.warn(err)
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     },
     /**
@@ -333,11 +333,11 @@ module.exports = {
         await WIKI.db.knex('userAvatars').where({ id: args.id }).del()
         WIKI.logger.debug(`Cleared user ${args.id} avatar successfully.`)
         return {
-          operation: graphHelper.generateSuccess('User avatar cleared successfully')
+          operation: generateSuccess('User avatar cleared successfully')
         }
       } catch (err) {
         WIKI.logger.warn(err)
-        return graphHelper.generateError(err)
+        return generateError(err)
       }
     }
   },

+ 3 - 3
server/graph/scalars/date.js → server/graph/scalars/date.mjs

@@ -1,12 +1,12 @@
-const gql = require('graphql')
-const { DateTime } = require('luxon')
+import gql from 'graphql'
+import { DateTime } from 'luxon'
 
 function parseDateTime (value) {
   const nDate = DateTime.fromISO(value)
   return nDate.isValid ? nDate : null
 }
 
-module.exports = new gql.GraphQLScalarType({
+export default new gql.GraphQLScalarType({
   name: 'Date',
   description: 'ISO date-time string at UTC',
   parseValue(value) {

+ 2 - 2
server/graph/scalars/json.js → server/graph/scalars/json.mjs

@@ -1,4 +1,4 @@
-const { Kind, GraphQLScalarType } = require('graphql')
+import { Kind, GraphQLScalarType } from 'graphql'
 
 function ensureObject (value) {
   if (typeof value !== 'object' || value === null || Array.isArray(value)) {
@@ -39,7 +39,7 @@ function parseObject (typeName, ast, variables) {
   return value
 }
 
-module.exports = new GraphQLScalarType({
+export default new GraphQLScalarType({
   name: 'JSON',
   description:
     'The `JSON` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).',

+ 2 - 2
server/graph/scalars/uuid.js → server/graph/scalars/uuid.mjs

@@ -1,4 +1,4 @@
-const { Kind, GraphQLScalarType } = require('graphql')
+import { Kind, GraphQLScalarType } from 'graphql'
 
 const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
 const nilUUID = '00000000-0000-0000-0000-000000000000'
@@ -7,7 +7,7 @@ function isUUID (value) {
   return uuidRegex.test(value) || nilUUID === value
 }
 
-module.exports = new GraphQLScalarType({
+export default new GraphQLScalarType({
   name: 'UUID',
   description: 'The `UUID` scalar type represents UUID values as specified by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122).',
   serialize: (value) => {

+ 4 - 2
server/helpers/brute-knex.js → server/helpers/brute-knex.mjs

@@ -1,6 +1,6 @@
-const AbstractClientStore = require('express-brute/lib/AbstractClientStore')
+import AbstractClientStore from 'express-brute/lib/AbstractClientStore.js'
 
-const KnexStore = module.exports = function (options) {
+const KnexStore = function (options) {
   options = options || Object.create(null)
 
   AbstractClientStore.apply(this, arguments)
@@ -146,3 +146,5 @@ KnexStore.defaultsKnex = {
     filename: './brute-knex.sqlite'
   }
 }
+
+export default KnexStore

+ 0 - 99
server/helpers/common.js

@@ -1,99 +0,0 @@
-const _ = require('lodash')
-const crypto = require('node:crypto')
-
-module.exports = {
-  /* eslint-disable promise/param-names */
-  createDeferred () {
-    let result, resolve, reject
-    return {
-      resolve: function (value) {
-        if (resolve) {
-          resolve(value)
-        } else {
-          result = result || new Promise(function (r) { r(value) })
-        }
-      },
-      reject: function (reason) {
-        if (reject) {
-          reject(reason)
-        } else {
-          result = result || new Promise(function (x, j) { j(reason) })
-        }
-      },
-      promise: new Promise(function (r, j) {
-        if (result) {
-          r(result)
-        } else {
-          resolve = r
-          reject = j
-        }
-      })
-    }
-  },
-  /**
-   * Decode a tree path
-   *
-   * @param {string} str String to decode
-   * @returns Decoded tree path
-   */
-  decodeTreePath (str) {
-    return str.replaceAll('_', '-').replaceAll('.', '/')
-  },
-  /**
-   * Encode a tree path
-   *
-   * @param {string} str String to encode
-   * @returns Encoded tree path
-   */
-  encodeTreePath (str) {
-    return str?.toLowerCase()?.replaceAll('-', '_')?.replaceAll('/', '.') || ''
-  },
-  /**
-   * Generate SHA-1 Hash of a string
-   *
-   * @param {string} str String to hash
-   * @returns Hashed string
-   */
-  generateHash (str) {
-    return crypto.createHash('sha1').update(str).digest('hex')
-  },
-  /**
-   * Get default value of type
-   *
-   * @param {any} type primitive type name
-   * @returns Default value
-   */
-  getTypeDefaultValue (type) {
-    switch (type.toLowerCase()) {
-      case 'string':
-        return ''
-      case 'number':
-        return 0
-      case 'boolean':
-        return false
-    }
-  },
-  parseModuleProps (props) {
-    return _.transform(props, (result, value, key) => {
-      let defaultValue = ''
-      if (_.isPlainObject(value)) {
-        defaultValue = !_.isNil(value.default) ? value.default : this.getTypeDefaultValue(value.type)
-      } else {
-        defaultValue = this.getTypeDefaultValue(value)
-      }
-      _.set(result, key, {
-        default: defaultValue,
-        type: (value.type || value).toLowerCase(),
-        title: value.title || _.startCase(key),
-        hint: value.hint || '',
-        enum: value.enum || false,
-        enumDisplay: value.enumDisplay || 'select',
-        multiline: value.multiline || false,
-        sensitive: value.sensitive || false,
-        icon: value.icon || 'rename',
-        order: value.order || 100
-      })
-      return result
-    }, {})
-  }
-}

+ 102 - 0
server/helpers/common.mjs

@@ -0,0 +1,102 @@
+import { isNil, isPlainObject, set, startCase, transform } from 'lodash-es'
+import crypto from 'node:crypto'
+
+/* eslint-disable promise/param-names */
+export function createDeferred () {
+  let result, resolve, reject
+  return {
+    resolve: function (value) {
+      if (resolve) {
+        resolve(value)
+      } else {
+        result = result || new Promise(function (r) { r(value) })
+      }
+    },
+    reject: function (reason) {
+      if (reject) {
+        reject(reason)
+      } else {
+        result = result || new Promise(function (x, j) { j(reason) })
+      }
+    },
+    promise: new Promise(function (r, j) {
+      if (result) {
+        r(result)
+      } else {
+        resolve = r
+        reject = j
+      }
+    })
+  }
+}
+
+/**
+ * Decode a tree path
+ *
+ * @param {string} str String to decode
+ * @returns Decoded tree path
+ */
+export function decodeTreePath (str) {
+  return str.replaceAll('_', '-').replaceAll('.', '/')
+}
+
+/**
+ * Encode a tree path
+ *
+ * @param {string} str String to encode
+ * @returns Encoded tree path
+ */
+export function encodeTreePath (str) {
+  return str?.toLowerCase()?.replaceAll('-', '_')?.replaceAll('/', '.') || ''
+}
+
+/**
+ * Generate SHA-1 Hash of a string
+ *
+ * @param {string} str String to hash
+ * @returns Hashed string
+ */
+export function generateHash (str) {
+  return crypto.createHash('sha1').update(str).digest('hex')
+}
+
+/**
+ * Get default value of type
+ *
+ * @param {any} type primitive type name
+ * @returns Default value
+ */
+export function getTypeDefaultValue (type) {
+  switch (type.toLowerCase()) {
+    case 'string':
+      return ''
+    case 'number':
+      return 0
+    case 'boolean':
+      return false
+  }
+}
+
+export function parseModuleProps (props) {
+  return transform(props, (result, value, key) => {
+    let defaultValue = ''
+    if (isPlainObject(value)) {
+      defaultValue = !isNil(value.default) ? value.default : this.getTypeDefaultValue(value.type)
+    } else {
+      defaultValue = this.getTypeDefaultValue(value)
+    }
+    set(result, key, {
+      default: defaultValue,
+      type: (value.type || value).toLowerCase(),
+      title: value.title || startCase(key),
+      hint: value.hint || '',
+      enum: value.enum || false,
+      enumDisplay: value.enumDisplay || 'select',
+      multiline: value.multiline || false,
+      sensitive: value.sensitive || false,
+      icon: value.icon || 'rename',
+      order: value.order || 100
+    })
+    return result
+  }, {})
+}

+ 3 - 5
server/helpers/config.js → server/helpers/config.mjs

@@ -1,10 +1,8 @@
-'use strict'
-
-const _ = require('lodash')
+import { replace } from 'lodash-es'
 
 const isoDurationReg = /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/
 
-module.exports = {
+export default {
   /**
    * Parse configuration value for environment vars
    *
@@ -16,7 +14,7 @@ module.exports = {
    * @returns Parse configuration value
    */
   parseConfigValue (cfg) {
-    return _.replace(
+    return replace(
       cfg,
       /\$\(([A-Z0-9_]+)(?::(.+))?\)/g,
       (fm, m, d) => { return process.env[m] || d }

+ 2 - 2
server/helpers/error.js → server/helpers/error.mjs

@@ -1,6 +1,6 @@
-const CustomError = require('custom-error-instance')
+import CustomError from 'custom-error-instance'
 
-module.exports = {
+export default {
   Custom (slug, message) {
     return CustomError(slug, { message })
   },

+ 0 - 21
server/helpers/graph.js

@@ -1,21 +0,0 @@
-const _ = require('lodash')
-
-module.exports = {
-  generateSuccess (msg) {
-    return {
-      succeeded: true,
-      errorCode: 0,
-      slug: 'ok',
-      message: _.defaultTo(msg, 'Operation succeeded.')
-    }
-  },
-  generateError (err, complete = true) {
-    const error = {
-      succeeded: false,
-      errorCode: _.isFinite(err.code) ? err.code : 1,
-      slug: err.name,
-      message: err.message || 'An unexpected error occured.'
-    }
-    return (complete) ? { operation: error } : error
-  }
-}

+ 20 - 0
server/helpers/graph.mjs

@@ -0,0 +1,20 @@
+import { defaultTo, isFinite } from 'lodash-es'
+
+export function generateSuccess (msg) {
+  return {
+    succeeded: true,
+    errorCode: 0,
+    slug: 'ok',
+    message: defaultTo(msg, 'Operation succeeded.')
+  }
+}
+
+export function generateError (err, complete = true) {
+  const error = {
+    succeeded: false,
+    errorCode: isFinite(err.code) ? err.code : 1,
+    slug: err.name,
+    message: err.message || 'An unexpected error occured.'
+  }
+  return (complete) ? { operation: error } : error
+}

+ 0 - 151
server/helpers/page.js

@@ -1,151 +0,0 @@
-const qs = require('querystring')
-const _ = require('lodash')
-const crypto = require('crypto')
-const path = require('path')
-
-const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i
-const localeFolderRegex = /^([a-z]{2}(?:-[a-z]{2})?\/)?(.*)/i
-// eslint-disable-next-line no-control-regex
-const unsafeCharsRegex = /[\x00-\x1f\x80-\x9f\\"|<>:*?]/
-
-const contentToExt = {
-  markdown: 'md',
-  html: 'html'
-}
-const extToContent = _.invert(contentToExt)
-
-module.exports = {
-  /**
-   * Parse raw url path and make it safe
-   */
-  parsePath (rawPath, opts = {}) {
-    let pathObj = {
-      // TODO: use site base lang
-      locale: 'en', // WIKI.config.lang.code,
-      path: 'home',
-      private: false,
-      privateNS: '',
-      explicitLocale: false
-    }
-
-    // Clean Path
-    rawPath = _.trim(qs.unescape(rawPath))
-    if (_.startsWith(rawPath, '/')) { rawPath = rawPath.substring(1) }
-    rawPath = rawPath.replace(unsafeCharsRegex, '')
-    if (rawPath === '') { rawPath = 'home' }
-
-    rawPath = rawPath.replace(/\\/g, '').replace(/\/\//g, '').replace(/\.\.+/ig, '')
-
-    // Extract Info
-    let pathParts = _.filter(_.split(rawPath, '/'), p => {
-      p = _.trim(p)
-      return !_.isEmpty(p) && p !== '..' && p !== '.'
-    })
-    if (pathParts[0].startsWith('_')) {
-      pathParts.shift()
-    }
-    if (localeSegmentRegex.test(pathParts[0])) {
-      pathObj.locale = pathParts[0]
-      pathObj.explicitLocale = true
-      pathParts.shift()
-    }
-
-    // Strip extension
-    if (opts.stripExt && pathParts.length > 0) {
-      const lastPart = _.last(pathParts)
-      if (lastPart.indexOf('.') > 0) {
-        pathParts.pop()
-        const lastPartMeta = path.parse(lastPart)
-        pathParts.push(lastPartMeta.name)
-      }
-    }
-
-    pathObj.path = _.join(pathParts, '/')
-    return pathObj
-  },
-  /**
-   * Generate unique hash from page
-   */
-  generateHash(opts) {
-    return crypto.createHash('sha1').update(`${opts.locale}|${opts.path}|${opts.privateNS}`).digest('hex')
-  },
-  /**
-   * Inject Page Metadata
-   */
-  injectPageMetadata(page) {
-    let meta = [
-      ['title', page.title],
-      ['description', page.description],
-      ['published', page.isPublished.toString()],
-      ['date', page.updatedAt],
-      ['tags', page.tags ? page.tags.map(t => t.tag).join(', ') : ''],
-      ['editor', page.editorKey],
-      ['dateCreated', page.createdAt]
-    ]
-    switch (page.contentType) {
-      case 'markdown':
-        return '---\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n---\n\n' + page.content
-      case 'html':
-        return '<!--\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n-->\n\n' + page.content
-      case 'json':
-        return {
-          ...page.content,
-          _meta: _.fromPairs(meta)
-        }
-      default:
-        return page.content
-    }
-  },
-  /**
-   * Check if path is a reserved path
-   */
-  isReservedPath(rawPath) {
-    const firstSection = _.head(rawPath.split('/'))
-    if (firstSection.length < 1) {
-      return true
-    } else if (localeSegmentRegex.test(firstSection)) {
-      return true
-    } else if (
-      _.some(WIKI.data.reservedPaths, p => {
-        return p === firstSection
-      })) {
-      return true
-    } else {
-      return false
-    }
-  },
-  /**
-   * Get file extension from content type
-   */
-  getFileExtension(contentType) {
-    return _.get(contentToExt, contentType, 'txt')
-  },
-  /**
-   * Get content type from file extension
-   */
-  getContentType (filePath) {
-    const ext = _.last(filePath.split('.'))
-    return _.get(extToContent, ext, false)
-  },
-  /**
-   * Get Page Meta object from disk path
-   */
-  getPagePath (filePath) {
-    let fpath = filePath
-    if (process.platform === 'win32') {
-      fpath = filePath.replace(/\\/g, '/')
-    }
-    let meta = {
-      locale: WIKI.config.lang.code,
-      path: _.initial(fpath.split('.')).join('')
-    }
-    const result = localeFolderRegex.exec(meta.path)
-    if (result[1]) {
-      meta = {
-        locale: result[1].replace('/', ''),
-        path: result[2]
-      }
-    }
-    return meta
-  }
-}

+ 155 - 0
server/helpers/page.mjs

@@ -0,0 +1,155 @@
+import qs from 'querystring'
+import { fromPairs, get, initial, invert, isEmpty, last } from 'lodash-es'
+import crypto from 'node:crypto'
+import path from 'node:path'
+
+const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i
+const localeFolderRegex = /^([a-z]{2}(?:-[a-z]{2})?\/)?(.*)/i
+// eslint-disable-next-line no-control-regex
+const unsafeCharsRegex = /[\x00-\x1f\x80-\x9f\\"|<>:*?]/
+
+const contentToExt = {
+  markdown: 'md',
+  html: 'html'
+}
+const extToContent = invert(contentToExt)
+
+/**
+ * Parse raw url path and make it safe
+ */
+export function parsePath (rawPath, opts = {}) {
+  const pathObj = {
+    // TODO: use site base lang
+    locale: 'en', // WIKI.config.lang.code,
+    path: 'home',
+    private: false,
+    privateNS: '',
+    explicitLocale: false
+  }
+
+  // Clean Path
+  rawPath = qs.unescape(rawPath).trim()
+  if (rawPath.startsWith('/')) { rawPath = rawPath.substring(1) }
+  rawPath = rawPath.replace(unsafeCharsRegex, '')
+  if (rawPath === '') { rawPath = 'home' }
+
+  rawPath = rawPath.replace(/\\/g, '').replace(/\/\//g, '').replace(/\.\.+/ig, '')
+
+  // Extract Info
+  let pathParts = rawPath.split('/').filter(p => {
+    p = p.trim()
+    return !isEmpty(p) && p !== '..' && p !== '.'
+  })
+  if (pathParts[0].startsWith('_')) {
+    pathParts.shift()
+  }
+  if (localeSegmentRegex.test(pathParts[0])) {
+    pathObj.locale = pathParts[0]
+    pathObj.explicitLocale = true
+    pathParts.shift()
+  }
+
+  // Strip extension
+  if (opts.stripExt && pathParts.length > 0) {
+    const lastPart = last(pathParts)
+    if (lastPart.indexOf('.') > 0) {
+      pathParts.pop()
+      const lastPartMeta = path.parse(lastPart)
+      pathParts.push(lastPartMeta.name)
+    }
+  }
+
+  pathObj.path = _.join(pathParts, '/')
+  return pathObj
+}
+
+/**
+ * Generate unique hash from page
+ */
+export function generateHash(opts) {
+  return crypto.createHash('sha1').update(`${opts.locale}|${opts.path}|${opts.privateNS}`).digest('hex')
+}
+
+/**
+ * Inject Page Metadata
+ */
+export function injectPageMetadata(page) {
+  const meta = [
+    ['title', page.title],
+    ['description', page.description],
+    ['published', page.isPublished.toString()],
+    ['date', page.updatedAt],
+    ['tags', page.tags ? page.tags.map(t => t.tag).join(', ') : ''],
+    ['editor', page.editorKey],
+    ['dateCreated', page.createdAt]
+  ]
+  switch (page.contentType) {
+    case 'markdown':
+      return '---\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n---\n\n' + page.content
+    case 'html':
+      return '<!--\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n-->\n\n' + page.content
+    case 'json':
+      return {
+        ...page.content,
+        _meta: fromPairs(meta)
+      }
+    default:
+      return page.content
+  }
+}
+
+/**
+ * Check if path is a reserved path
+ */
+export function isReservedPath(rawPath) {
+  const firstSection = _.head(rawPath.split('/'))
+  if (firstSection.length < 1) {
+    return true
+  } else if (localeSegmentRegex.test(firstSection)) {
+    return true
+  } else if (
+    WIKI.data.reservedPaths.some(p => {
+      return p === firstSection
+    })) {
+    return true
+  } else {
+    return false
+  }
+}
+
+/**
+ * Get file extension from content type
+ */
+export function getFileExtension(contentType) {
+  return get(contentToExt, contentType, 'txt')
+}
+
+/**
+ * Get content type from file extension
+ */
+export function getContentType (filePath) {
+  const ext = last(filePath.split('.'))
+  return get(extToContent, ext, false)
+}
+
+/**
+ * Get Page Meta object from disk path
+ */
+export function getPagePath (filePath) {
+  let fpath = filePath
+  if (process.platform === 'win32') {
+    fpath = filePath.replace(/\\/g, '/')
+  }
+  let meta = {
+    locale: WIKI.config.lang.code,
+    path: initial(fpath.split('.')).join('')
+  }
+  const result = localeFolderRegex.exec(meta.path)
+  if (result[1]) {
+    meta = {
+      locale: result[1].replace('/', ''),
+      path: result[2]
+    }
+  }
+  return meta
+}

+ 0 - 38
server/helpers/security.js

@@ -1,38 +0,0 @@
-const util = require('node:util')
-const crypto = require('node:crypto')
-const randomBytes = util.promisify(crypto.randomBytes)
-const passportJWT = require('passport-jwt')
-
-module.exports = {
-  sanitizeCommitUser (user) {
-    // let wlist = new RegExp('[^a-zA-Z0-9-_.\',& ' + appdata.regex.cjk + appdata.regex.arabic + ']', 'g')
-    // return {
-    //   name: _.chain(user.name).replace(wlist, '').trim().value(),
-    //   email: appconfig.git.showUserEmail ? user.email : appconfig.git.serverEmail
-    // }
-  },
-  /**
-   * Generate a random token
-   *
-   * @param {any} length
-   * @returns
-   */
-  async generateToken (length) {
-    return (await randomBytes(length)).toString('hex')
-  },
-
-  extractJWT: passportJWT.ExtractJwt.fromExtractors([
-    passportJWT.ExtractJwt.fromAuthHeaderAsBearerToken(),
-    (req) => {
-      let token = null
-      if (req && req.cookies) {
-        token = req.cookies['jwt']
-      }
-      // Force uploads to use Auth headers
-      if (req.path.toLowerCase() === '/u') {
-        return null
-      }
-      return token
-    }
-  ])
-}

+ 38 - 0
server/helpers/security.mjs

@@ -0,0 +1,38 @@
+import util from 'node:util'
+import crypto from 'node:crypto'
+import passportJWT from 'passport-jwt'
+
+const randomBytes = util.promisify(crypto.randomBytes)
+
+export function sanitizeCommitUser (user) {
+  // let wlist = new RegExp('[^a-zA-Z0-9-_.\',& ' + appdata.regex.cjk + appdata.regex.arabic + ']', 'g')
+  // return {
+  //   name: _.chain(user.name).replace(wlist, '').trim().value(),
+  //   email: appconfig.git.showUserEmail ? user.email : appconfig.git.serverEmail
+  // }
+}
+
+/**
+ * Generate a random token
+ *
+ * @param {any} length
+ * @returns
+ */
+export async function generateToken (length) {
+  return (await randomBytes(length)).toString('hex')
+}
+
+export const extractJWT = passportJWT.ExtractJwt.fromExtractors([
+  passportJWT.ExtractJwt.fromAuthHeaderAsBearerToken(),
+  (req) => {
+    let token = null
+    if (req && req.cookies) {
+      token = req.cookies['jwt']
+    }
+    // Force uploads to use Auth headers
+    if (req.path.toLowerCase() === '/u') {
+      return null
+    }
+    return token
+  }
+])

+ 16 - 12
server/index.js → server/index.mjs

@@ -3,30 +3,34 @@
 // Licensed under AGPLv3
 // ===========================================
 
-const path = require('path')
-const { DateTime } = require('luxon')
-const semver = require('semver')
-const nanoid = require('nanoid').customAlphabet('1234567890abcdef', 10)
-const fs = require('fs-extra')
+import path from 'node:path'
+import { DateTime } from 'luxon'
+import semver from 'semver'
+import { customAlphabet } from 'nanoid'
+import fse from 'fs-extra'
+import configSvc from './core/config.mjs'
+import kernel from './core/kernel.mjs'
+import logger from './core/logger.mjs'
+
+const nanoid = customAlphabet('1234567890abcdef', 10)
 
 if (!semver.satisfies(process.version, '>=18')) {
   console.error('ERROR: Node.js 18.x or later required!')
   process.exit(1)
 }
 
-if (fs.pathExistsSync('./package.json')) {
+if (fse.pathExistsSync('./package.json')) {
   console.error('ERROR: Must run server from the parent directory!')
   process.exit(1)
 }
 
-let WIKI = {
+const WIKI = {
   IS_DEBUG: process.env.NODE_ENV === 'development',
   ROOTPATH: process.cwd(),
   INSTANCE_ID: nanoid(10),
   SERVERPATH: path.join(process.cwd(), 'server'),
-  Error: require('./helpers/error'),
-  configSvc: require('./core/config'),
-  kernel: require('./core/kernel'),
+  configSvc,
+  kernel,
   sites: {},
   sitesMappings: {},
   startedAt: DateTime.utc(),
@@ -37,13 +41,13 @@ let WIKI = {
 }
 global.WIKI = WIKI
 
-WIKI.configSvc.init()
+await WIKI.configSvc.init()
 
 // ----------------------------------------
 // Init Logger
 // ----------------------------------------
 
-WIKI.logger = require('./core/logger').init()
+WIKI.logger = logger.init()
 
 // ----------------------------------------
 // Start Kernel

+ 0 - 20
server/locales/README.md

@@ -1,20 +0,0 @@
-## IMPORTANT
-
-Localization files are not stored into files!
-
-Contact us on Gitter to request access to the translation web service: https://gitter.im/Requarks/wiki
-
-## Development Mode
-
-If you need to add new keys and test them live, simply create a {LANG}.yml file in this folder containing the values you want to test. e.g.:
-
-### en.yml
-```yml
-admin:
-  api.title: 'API Access'
-  auth.title: 'Authentication'
-```
-
-The official localization keys will still be loaded first, but your local files will overwrite any existing keys (and add new ones).
-
-Note that you must restart Wiki.js to load any changes made to the files, which happens automatically on save when in dev mode.

+ 0 - 46
server/middlewares/security.js

@@ -1,46 +0,0 @@
-/* global WIKI */
-
-/**
- * Security Middleware
- *
- * @param      {Express Request}   req     Express request object
- * @param      {Express Response}  res     Express response object
- * @param      {Function}          next    next callback function
- * @return     {any}               void
- */
-module.exports = function (req, res, next) {
-  // -> Disable X-Powered-By
-  req.app.disable('x-powered-by')
-
-  // -> Disable Frame Embedding
-  if (WIKI.config.security.securityIframe) {
-    res.set('X-Frame-Options', 'deny')
-  }
-
-  // -> Re-enable XSS Fitler if disabled
-  res.set('X-XSS-Protection', '1; mode=block')
-
-  // -> Disable MIME-sniffing
-  res.set('X-Content-Type-Options', 'nosniff')
-
-  // -> Disable IE Compatibility Mode
-  res.set('X-UA-Compatible', 'IE=edge')
-
-  // -> Disables referrer header when navigating to a different origin
-  if (WIKI.config.security.securityReferrerPolicy) {
-    res.set('Referrer-Policy', 'same-origin')
-  }
-
-  // -> Enforce HSTS
-  if (WIKI.config.security.securityHSTS) {
-    res.set('Strict-Transport-Security', `max-age=${WIKI.config.security.securityHSTSDuration}; includeSubDomains`)
-  }
-
-  // -> Prevent Open Redirect from user provided URL
-  if (WIKI.config.security.securityOpenRedirect) {
-    // Strips out all repeating / character in the provided URL
-    req.url = req.url.replace(/(\/)(?=\/*\1)/g, '')
-  }
-
-  return next()
-}

+ 0 - 19
server/middlewares/seo.js

@@ -1,19 +0,0 @@
-const _ = require('lodash')
-
-/**
- * SEO Middleware
- *
- * @param      {Express Request}   req     Express request object
- * @param      {Express Response}  res     Express response object
- * @param      {Function}          next    next callback function
- * @return     {any}               void
- */
-module.exports = function (req, res, next) {
-  if (req.path.length > 1 && _.endsWith(req.path, '/')) {
-    let query = req.url.slice(req.path.length) || ''
-    res.redirect(301, req.path.slice(0, -1) + query)
-  } else {
-    _.set(res.locals, 'pageMeta.url', `${WIKI.config.host}${req.path}`)
-    return next()
-  }
-}

+ 17 - 17
server/models/analytics.js → server/models/analytics.mjs

@@ -1,14 +1,14 @@
-const Model = require('objection').Model
-const fs = require('fs-extra')
-const path = require('path')
-const _ = require('lodash')
-const yaml = require('js-yaml')
-const commonHelper = require('../helpers/common')
+import { Model } from 'objection'
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import { defaultTo, forOwn, isBoolean, replace, sortBy } from 'lodash-es'
+import yaml from 'js-yaml'
+import { parseModuleProps } from '../helpers/common.mjs'
 
 /**
  * Analytics model
  */
-module.exports = class Analytics extends Model {
+export class Analytics extends Model {
   static get tableName() { return 'analytics' }
 
   static get jsonSchema () {
@@ -29,8 +29,8 @@ module.exports = class Analytics extends Model {
   }
 
   static async getProviders(isEnabled) {
-    const providers = await WIKI.db.analytics.query().where(_.isBoolean(isEnabled) ? { isEnabled } : {})
-    return _.sortBy(providers, ['module'])
+    const providers = await WIKI.db.analytics.query().where(isBoolean(isEnabled) ? { isEnabled } : {})
+    return sortBy(providers, ['module'])
   }
 
   static async refreshProvidersFromDisk() {
@@ -42,7 +42,7 @@ module.exports = class Analytics extends Model {
         const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/analytics', dir, 'definition.yml'), 'utf8')
         const defParsed = yaml.load(def)
         defParsed.key = dir
-        defParsed.props = commonHelper.parseModuleProps(defParsed.props)
+        defParsed.props = parseModuleProps(defParsed.props)
         WIKI.data.analytics.push(defParsed)
         WIKI.logger.debug(`Loaded analytics module definition ${dir}: [ OK ]`)
       }
@@ -72,14 +72,14 @@ module.exports = class Analytics extends Model {
       for (let provider of providers) {
         const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/analytics', provider.key, 'code.yml'), 'utf8')
         let code = yaml.safeLoad(def)
-        code.head = _.defaultTo(code.head, '')
-        code.bodyStart = _.defaultTo(code.bodyStart, '')
-        code.bodyEnd = _.defaultTo(code.bodyEnd, '')
+        code.head = defaultTo(code.head, '')
+        code.bodyStart = defaultTo(code.bodyStart, '')
+        code.bodyEnd = defaultTo(code.bodyEnd, '')
 
-        _.forOwn(provider.config, (value, key) => {
-          code.head = _.replace(code.head, new RegExp(`{{${key}}}`, 'g'), value)
-          code.bodyStart = _.replace(code.bodyStart, `{{${key}}}`, value)
-          code.bodyEnd = _.replace(code.bodyEnd, `{{${key}}}`, value)
+        forOwn(provider.config, (value, key) => {
+          code.head = replace(code.head, new RegExp(`{{${key}}}`, 'g'), value)
+          code.bodyStart = replace(code.bodyStart, `{{${key}}}`, value)
+          code.bodyEnd = replace(code.bodyEnd, `{{${key}}}`, value)
         })
 
         analyticsCode.head += code.head

+ 5 - 5
server/models/apiKeys.js → server/models/apiKeys.mjs

@@ -1,14 +1,14 @@
 /* global WIKI */
 
-const Model = require('objection').Model
-const { DateTime } = require('luxon')
-const ms = require('ms')
-const jwt = require('jsonwebtoken')
+import { Model } from 'objection'
+import { DateTime } from 'luxon'
+import ms from 'ms'
+import jwt from 'jsonwebtoken'
 
 /**
  * Users model
  */
-module.exports = class ApiKey extends Model {
+export class ApiKey extends Model {
   static get tableName() { return 'apiKeys' }
 
   static get jsonSchema () {

+ 17 - 24
server/models/assets.js → server/models/assets.mjs

@@ -1,14 +1,15 @@
-const Model = require('objection').Model
-const moment = require('moment')
-const path = require('path')
-const fs = require('fs-extra')
-const _ = require('lodash')
-const commonHelper = require('../helpers/common')
+import { Model } from 'objection'
+import path from 'path'
+import fse from 'fs-extra'
+import { startsWith } from 'lodash-es'
+import { generateHash } from '../helpers/common.mjs'
+
+import { User } from './users.mjs'
 
 /**
  * Users model
  */
-module.exports = class Asset extends Model {
+export class Asset extends Model {
   static get tableName() { return 'assets' }
 
   static get jsonSchema () {
@@ -34,19 +35,11 @@ module.exports = class Asset extends Model {
     return {
       author: {
         relation: Model.BelongsToOneRelation,
-        modelClass: require('./users'),
+        modelClass: User,
         join: {
           from: 'assets.authorId',
           to: 'users.id'
         }
-      },
-      folder: {
-        relation: Model.BelongsToOneRelation,
-        modelClass: require('./assetFolders'),
-        join: {
-          from: 'assets.folderId',
-          to: 'assetFolders.id'
-        }
       }
     }
   }
@@ -72,7 +65,7 @@ module.exports = class Asset extends Model {
   }
 
   async deleteAssetCache() {
-    await fs.remove(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${this.hash}.dat`))
+    await fse.remove(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${this.hash}.dat`))
   }
 
   static async upload(opts) {
@@ -88,7 +81,7 @@ module.exports = class Asset extends Model {
     let assetRow = {
       filename: opts.originalname,
       ext: fileInfo.ext,
-      kind: _.startsWith(opts.mimetype, 'image/') ? 'image' : 'binary',
+      kind: startsWith(opts.mimetype, 'image/') ? 'image' : 'binary',
       mime: opts.mimetype,
       fileSize: opts.size,
       folderId: opts.folderId
@@ -112,7 +105,7 @@ module.exports = class Asset extends Model {
 
     // Save asset data
     try {
-      const fileBuffer = await fs.readFile(opts.path)
+      const fileBuffer = await fse.readFile(opts.path)
 
       if (asset) {
         // Patch existing asset
@@ -166,7 +159,7 @@ module.exports = class Asset extends Model {
       .select('tree.*', 'assets.preview', 'assets.previewState')
       .innerJoin('assets', 'tree.id', 'assets.id')
       .where(id ? { 'tree.id': id } : {
-        'tree.hash': commonHelper.generateHash(path),
+        'tree.hash': generateHash(path),
         'tree.localeCode': locale,
         'tree.siteId': siteId
       })
@@ -202,7 +195,7 @@ module.exports = class Asset extends Model {
 
   static async getAssetFromCache(assetPath, cachePath, res) {
     try {
-      await fs.access(cachePath, fs.constants.R_OK)
+      await fse.access(cachePath, fse.constants.R_OK)
     } catch (err) {
       return false
     }
@@ -217,7 +210,7 @@ module.exports = class Asset extends Model {
         path: assetPath
       }
     })
-    for (let location of _.filter(localLocations, location => Boolean(location.path))) {
+    for (let location of localLocations.filter(location => Boolean(location.path))) {
       const assetExists = await WIKI.db.assets.getAssetFromCache(assetPath, location.path, res)
       if (assetExists) {
         return true
@@ -232,13 +225,13 @@ module.exports = class Asset extends Model {
       const assetData = await WIKI.db.knex('assetData').where('id', asset.id).first()
       res.type(asset.ext)
       res.send(assetData.data)
-      await fs.outputFile(cachePath, assetData.data)
+      await fse.outputFile(cachePath, assetData.data)
     } else {
       res.sendStatus(404)
     }
   }
 
   static async flushTempUploads() {
-    return fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `uploads`))
+    return fse.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `uploads`))
   }
 }

+ 10 - 10
server/models/authentication.js → server/models/authentication.mjs

@@ -1,14 +1,14 @@
-const Model = require('objection').Model
-const fs = require('fs-extra')
-const path = require('path')
-const _ = require('lodash')
-const yaml = require('js-yaml')
-const commonHelper = require('../helpers/common')
+import { Model } from 'objection'
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import { get } from 'lodash-es'
+import yaml from 'js-yaml'
+import { parseModuleProps } from '../helpers/common.mjs'
 
 /**
  * Authentication model
  */
-module.exports = class Authentication extends Model {
+export class Authentication extends Model {
   static get tableName() { return 'authentication' }
 
   static get jsonSchema () {
@@ -37,8 +37,8 @@ module.exports = class Authentication extends Model {
     const strategies = await WIKI.db.authentication.query().where(enabledOnly ? { isEnabled: true } : {})
     return strategies.map(str => ({
       ...str,
-      domainWhitelist: _.get(str.domainWhitelist, 'v', []),
-      autoEnrollGroups: _.get(str.autoEnrollGroups, 'v', [])
+      domainWhitelist: get(str.domainWhitelist, 'v', []),
+      autoEnrollGroups: get(str.autoEnrollGroups, 'v', [])
     }))
   }
 
@@ -52,7 +52,7 @@ module.exports = class Authentication extends Model {
         const defParsed = yaml.load(def)
         if (!defParsed.isAvailable) { continue }
         defParsed.key = dir
-        defParsed.props = commonHelper.parseModuleProps(defParsed.props)
+        defParsed.props = parseModuleProps(defParsed.props)
         WIKI.data.authentication.push(defParsed)
         WIKI.logger.debug(`Loaded authentication module definition ${dir}: [ OK ]`)
       }

+ 19 - 19
server/models/commentProviders.js → server/models/commentProviders.mjs

@@ -1,14 +1,14 @@
-const Model = require('objection').Model
-const fs = require('fs-extra')
-const path = require('path')
-const _ = require('lodash')
-const yaml = require('js-yaml')
-const commonHelper = require('../helpers/common')
+import { Model } from 'objection'
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import { defaultTo, find, forOwn, isBoolean, replace, sortBy } from 'lodash-es'
+import yaml from 'js-yaml'
+import { parseModuleProps } from '../helpers/common.mjs'
 
 /**
  * CommentProvider model
  */
-module.exports = class CommentProvider extends Model {
+export class CommentProvider extends Model {
   static get tableName() { return 'commentProviders' }
   static get idColumn() { return 'key' }
 
@@ -33,8 +33,8 @@ module.exports = class CommentProvider extends Model {
   }
 
   static async getProviders(isEnabled) {
-    const providers = await WIKI.db.commentProviders.query().where(_.isBoolean(isEnabled) ? { isEnabled } : {})
-    return _.sortBy(providers, ['module'])
+    const providers = await WIKI.db.commentProviders.query().where(isBoolean(isEnabled) ? { isEnabled } : {})
+    return sortBy(providers, ['module'])
   }
 
   static async refreshProvidersFromDisk() {
@@ -46,7 +46,7 @@ module.exports = class CommentProvider extends Model {
         const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/comments', dir, 'definition.yml'), 'utf8')
         const defParsed = yaml.load(def)
         defParsed.key = dir
-        defParsed.props = commonHelper.parseModuleProps(defParsed.props)
+        defParsed.props = parseModuleProps(defParsed.props)
         WIKI.data.commentProviders.push(defParsed)
         WIKI.logger.debug(`Loaded comments provider module definition ${dir}: [ OK ]`)
       }
@@ -62,7 +62,7 @@ module.exports = class CommentProvider extends Model {
     const commentProvider = await WIKI.db.commentProviders.query().findOne('isEnabled', true)
     if (commentProvider) {
       WIKI.data.commentProvider = {
-        ..._.find(WIKI.data.commentProviders, ['key', commentProvider.module]),
+        ...find(WIKI.data.commentProviders, ['key', commentProvider.module]),
         head: '',
         bodyStart: '',
         bodyEnd: '',
@@ -72,14 +72,14 @@ module.exports = class CommentProvider extends Model {
       if (WIKI.data.commentProvider.codeTemplate) {
         const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/comments', commentProvider.key, 'code.yml'), 'utf8')
         let code = yaml.safeLoad(def)
-        code.head = _.defaultTo(code.head, '')
-        code.body = _.defaultTo(code.body, '')
-        code.main = _.defaultTo(code.main, '')
+        code.head = defaultTo(code.head, '')
+        code.body = defaultTo(code.body, '')
+        code.main = defaultTo(code.main, '')
 
-        _.forOwn(commentProvider.config, (value, key) => {
-          code.head = _.replace(code.head, new RegExp(`{{${key}}}`, 'g'), value)
-          code.body = _.replace(code.body, new RegExp(`{{${key}}}`, 'g'), value)
-          code.main = _.replace(code.main, new RegExp(`{{${key}}}`, 'g'), value)
+        forOwn(commentProvider.config, (value, key) => {
+          code.head = replace(code.head, new RegExp(`{{${key}}}`, 'g'), value)
+          code.body = replace(code.body, new RegExp(`{{${key}}}`, 'g'), value)
+          code.main = replace(code.main, new RegExp(`{{${key}}}`, 'g'), value)
         })
 
         WIKI.data.commentProvider.head = code.head
@@ -88,7 +88,7 @@ module.exports = class CommentProvider extends Model {
       } else {
         WIKI.data.commentProvider = {
           ...WIKI.data.commentProvider,
-          ...require(`../modules/comments/${commentProvider.key}/comment`),
+          ...(await import(`../modules/comments/${commentProvider.key}/comment.mjs`)),
           config: commentProvider.config
         }
         await WIKI.data.commentProvider.init()

+ 10 - 8
server/models/comments.js → server/models/comments.mjs

@@ -1,11 +1,13 @@
-const Model = require('objection').Model
-const validate = require('validate.js')
-const _ = require('lodash')
+import { Model } from 'objection'
+import validate from 'validate.js'
+
+import { Page } from './pages.mjs'
+import { User } from './users.mjs'
 
 /**
  * Comments model
  */
-module.exports = class Comment extends Model {
+export class Comment extends Model {
   static get tableName() { return 'comments' }
 
   static get jsonSchema () {
@@ -30,7 +32,7 @@ module.exports = class Comment extends Model {
     return {
       author: {
         relation: Model.BelongsToOneRelation,
-        modelClass: require('./users'),
+        modelClass: User,
         join: {
           from: 'comments.authorId',
           to: 'users.id'
@@ -38,7 +40,7 @@ module.exports = class Comment extends Model {
       },
       page: {
         relation: Model.BelongsToOneRelation,
-        modelClass: require('./pages'),
+        modelClass: Page,
         join: {
           from: 'comments.pageId',
           to: 'pages.id'
@@ -62,7 +64,7 @@ module.exports = class Comment extends Model {
     // -> Input validation
     if (user.id === 2) {
       const validation = validate({
-        email: _.toLower(guestEmail),
+        email: guestEmail.toLowerCase(),
         name: guestName
       }, {
         email: {
@@ -87,7 +89,7 @@ module.exports = class Comment extends Model {
       }
     }
 
-    content = _.trim(content)
+    content = content.trim()
     if (content.length < 2) {
       throw new WIKI.Error.CommentContentMissing()
     }

+ 5 - 3
server/models/groups.js → server/models/groups.mjs

@@ -1,9 +1,11 @@
-const Model = require('objection').Model
+import { Model } from 'objection'
+
+import { User } from './users.mjs'
 
 /**
  * Groups model
  */
-module.exports = class Group extends Model {
+export class Group extends Model {
   static get tableName() { return 'groups' }
 
   static get jsonSchema () {
@@ -30,7 +32,7 @@ module.exports = class Group extends Model {
     return {
       users: {
         relation: Model.ManyToManyRelation,
-        modelClass: require('./users'),
+        modelClass: User,
         join: {
           from: 'groups.id',
           through: {

+ 2 - 2
server/models/hooks.js → server/models/hooks.mjs

@@ -1,9 +1,9 @@
-const Model = require('objection').Model
+import { Model } from 'objection'
 
 /**
  * Hook model
  */
-module.exports = class Hook extends Model {
+export class Hook extends Model {
   static get tableName () { return 'hooks' }
 
   static get jsonAttributes () {

+ 45 - 0
server/models/index.mjs

@@ -0,0 +1,45 @@
+import { Analytics } from './analytics.mjs'
+import { ApiKey } from './apiKeys.mjs'
+import { Asset } from './assets.mjs'
+import { Authentication } from './authentication.mjs'
+import { CommentProvider } from './commentProviders.mjs'
+import { Comment } from './comments.mjs'
+import { Group } from './groups.mjs'
+import { Hook } from './hooks.mjs'
+import { Locale } from './locales.mjs'
+import { Navigation } from './navigation.mjs'
+import { PageHistory } from './pageHistory.mjs'
+import { PageLink } from './pageLinks.mjs'
+import { Page } from './pages.mjs'
+import { Renderer } from './renderers.mjs'
+import { Setting } from './settings.mjs'
+import { Site } from './sites.mjs'
+import { Storage } from './storage.mjs'
+import { Tag } from './tags.mjs'
+import { Tree } from './tree.mjs'
+import { UserKey } from './userKeys.mjs'
+import { User } from './users.mjs'
+
+export default {
+  analytics: Analytics,
+  apiKeys: ApiKey,
+  assets: Asset,
+  authentication: Authentication,
+  commentProviders: CommentProvider,
+  comments: Comment,
+  groups: Group,
+  hooks: Hook,
+  locales: Locale,
+  navigation: Navigation,
+  pageHistory: PageHistory,
+  pageLinks: PageLink,
+  pages: Page,
+  renderers: Renderer,
+  settings: Setting,
+  sites: Site,
+  storage: Storage,
+  tags: Tag,
+  tree: Tree,
+  userKeys: UserKey,
+  users: User
+}

+ 2 - 2
server/models/locales.js → server/models/locales.mjs

@@ -1,9 +1,9 @@
-const Model = require('objection').Model
+import { Model } from 'objection'
 
 /**
  * Locales model
  */
-module.exports = class Locale extends Model {
+export class Locale extends Model {
   static get tableName() { return 'locales' }
   static get idColumn() { return 'code' }
 

+ 5 - 5
server/models/navigation.js → server/models/navigation.mjs

@@ -1,10 +1,10 @@
-const Model = require('objection').Model
-const _ = require('lodash')
+import { Model } from 'objection'
+import { has } from 'lodash-es'
 
 /**
  * Navigation model
  */
-module.exports = class Navigation extends Model {
+export class Navigation extends Model {
   static get tableName() { return 'navigation' }
   static get idColumn() { return 'key' }
 
@@ -30,7 +30,7 @@ module.exports = class Navigation extends Model {
     const navTree = await WIKI.db.navigation.query().findOne('key', `site`)
     if (navTree) {
       // Check for pre-2.3 format
-      if (_.has(navTree.config[0], 'kind')) {
+      if (has(navTree.config[0], 'kind')) {
         navTree.config = [{
           locale: 'en',
           items: navTree.config.map(item => ({
@@ -58,7 +58,7 @@ module.exports = class Navigation extends Model {
   }
 
   static getAuthorizedItems(tree = [], groups = []) {
-    return _.filter(tree, leaf => {
+    return tree.filter(leaf => {
       return leaf.visibilityMode === 'all' || _.intersection(leaf.visibilityGroups, groups).length > 0
     })
   }

+ 16 - 11
server/models/pageHistory.js → server/models/pageHistory.mjs

@@ -1,11 +1,16 @@
-const Model = require('objection').Model
-const _ = require('lodash')
-const { DateTime, Duration } = require('luxon')
+import { Model } from 'objection'
+import { get, reduce, reverse } from 'lodash-es'
+import { DateTime, Duration } from 'luxon'
+
+import { Locale } from './locales.mjs'
+import { Page } from './pages.mjs'
+import { User } from './users.mjs'
+import { Tag } from './tags.mjs'
 
 /**
  * Page History model
  */
-module.exports = class PageHistory extends Model {
+export class PageHistory extends Model {
   static get tableName() { return 'pageHistory' }
 
   static get jsonSchema () {
@@ -34,7 +39,7 @@ module.exports = class PageHistory extends Model {
     return {
       tags: {
         relation: Model.ManyToManyRelation,
-        modelClass: require('./tags'),
+        modelClass: Tag,
         join: {
           from: 'pageHistory.id',
           through: {
@@ -46,7 +51,7 @@ module.exports = class PageHistory extends Model {
       },
       page: {
         relation: Model.BelongsToOneRelation,
-        modelClass: require('./pages'),
+        modelClass: Page,
         join: {
           from: 'pageHistory.pageId',
           to: 'pages.id'
@@ -54,7 +59,7 @@ module.exports = class PageHistory extends Model {
       },
       author: {
         relation: Model.BelongsToOneRelation,
-        modelClass: require('./users'),
+        modelClass: User,
         join: {
           from: 'pageHistory.authorId',
           to: 'users.id'
@@ -62,7 +67,7 @@ module.exports = class PageHistory extends Model {
       },
       locale: {
         relation: Model.BelongsToOneRelation,
-        modelClass: require('./locales'),
+        modelClass: Locale,
         join: {
           from: 'pageHistory.localeCode',
           to: 'locales.code'
@@ -189,16 +194,16 @@ module.exports = class PageHistory extends Model {
     }
 
     return {
-      trail: _.reduce(_.reverse(history.results), (res, ph) => {
+      trail: reduce(reverse(history.results), (res, ph) => {
         let actionType = 'edit'
         let valueBefore = null
         let valueAfter = null
 
         if (!prevPh && history.total < upperLimit) {
           actionType = 'initial'
-        } else if (_.get(prevPh, 'path', '') !== ph.path) {
+        } else if (get(prevPh, 'path', '') !== ph.path) {
           actionType = 'move'
-          valueBefore = _.get(prevPh, 'path', '')
+          valueBefore = get(prevPh, 'path', '')
           valueAfter = ph.path
         }
 

+ 5 - 3
server/models/pageLinks.js → server/models/pageLinks.mjs

@@ -1,9 +1,11 @@
-const Model = require('objection').Model
+import { Model } from 'objection'
+
+import { Page } from './pages.mjs'
 
 /**
  * Users model
  */
-module.exports = class PageLink extends Model {
+export class PageLink extends Model {
   static get tableName() { return 'pageLinks' }
 
   static get jsonSchema () {
@@ -23,7 +25,7 @@ module.exports = class PageLink extends Model {
     return {
       page: {
         relation: Model.BelongsToOneRelation,
-        modelClass: require('./pages'),
+        modelClass: Page,
         join: {
           from: 'pageLinks.pageId',
           to: 'pages.id'

+ 42 - 37
server/models/pages.js → server/models/pages.mjs

@@ -1,17 +1,22 @@
-const Model = require('objection').Model
-const _ = require('lodash')
-const JSBinType = require('js-binary').Type
-const pageHelper = require('../helpers/page')
-const path = require('path')
-const fs = require('fs-extra')
-const yaml = require('js-yaml')
-const striptags = require('striptags')
-const emojiRegex = require('emoji-regex')
-const he = require('he')
-const CleanCSS = require('clean-css')
-const TurndownService = require('turndown')
-const turndownPluginGfm = require('@joplin/turndown-plugin-gfm').gfm
-const cheerio = require('cheerio')
+import { Model } from 'objection'
+import { find, get, has, isEmpty, isString, pick } from 'lodash-es'
+import { Type as JSBinType } from 'js-binary'
+import { generateHash, getFileExtension, injectPageMetadata } from '../helpers/page.mjs'
+import path from 'node:path'
+import fse from 'fs-extra'
+import yaml from 'js-yaml'
+import striptags from 'striptags'
+import emojiRegex from 'emoji-regex'
+import he from 'he'
+import CleanCSS from 'clean-css'
+import TurndownService from 'turndown'
+import { gfm as turndownPluginGfm } from '@joplin/turndown-plugin-gfm'
+import cheerio from 'cheerio'
+
+import { Locale } from './locales.mjs'
+import { PageLink } from './pageLinks.mjs'
+import { Tag } from './tags.mjs'
+import { User } from './users.mjs'
 
 const pageRegex = /^[a-zA0-90-9-_/]*$/
 
@@ -27,7 +32,7 @@ const punctuationRegex = /[!,:;/\\_+\-=()&#@<>$~%^*[\]{}"'|]+|(\.\s)|(\s\.)/ig
 /**
  * Pages model
  */
-module.exports = class Page extends Model {
+export class Page extends Model {
   static get tableName() { return 'pages' }
 
   static get jsonSchema () {
@@ -62,7 +67,7 @@ module.exports = class Page extends Model {
     return {
       tags: {
         relation: Model.ManyToManyRelation,
-        modelClass: require('./tags'),
+        modelClass: Tag,
         join: {
           from: 'pages.id',
           through: {
@@ -74,7 +79,7 @@ module.exports = class Page extends Model {
       },
       links: {
         relation: Model.HasManyRelation,
-        modelClass: require('./pageLinks'),
+        modelClass: PageLink,
         join: {
           from: 'pages.id',
           to: 'pageLinks.pageId'
@@ -82,7 +87,7 @@ module.exports = class Page extends Model {
       },
       author: {
         relation: Model.BelongsToOneRelation,
-        modelClass: require('./users'),
+        modelClass: User,
         join: {
           from: 'pages.authorId',
           to: 'users.id'
@@ -90,7 +95,7 @@ module.exports = class Page extends Model {
       },
       creator: {
         relation: Model.BelongsToOneRelation,
-        modelClass: require('./users'),
+        modelClass: User,
         join: {
           from: 'pages.creatorId',
           to: 'users.id'
@@ -98,7 +103,7 @@ module.exports = class Page extends Model {
       },
       locale: {
         relation: Model.BelongsToOneRelation,
-        modelClass: require('./locales'),
+        modelClass: Locale,
         join: {
           from: 'pages.localeCode',
           to: 'locales.code'
@@ -162,7 +167,7 @@ module.exports = class Page extends Model {
    * @returns {string} Page Contents with Injected Metadata
    */
   injectMetadata () {
-    return pageHelper.injectPageMetadata(this)
+    return injectPageMetadata(this)
   }
 
   /**
@@ -171,7 +176,7 @@ module.exports = class Page extends Model {
    * @returns {string} File Extension
    */
   getFileExtension() {
-    return pageHelper.getFileExtension(this.contentType)
+    return getFileExtension(this.contentType)
   }
 
   /**
@@ -312,7 +317,7 @@ module.exports = class Page extends Model {
       contentType: WIKI.data.editors[opts.editor]?.contentType ?? 'text',
       description: opts.description,
       editor: opts.editor,
-      hash: pageHelper.generateHash({ path: opts.path, locale: opts.locale }),
+      hash: generateHash({ path: opts.path, locale: opts.locale }),
       icon: opts.icon,
       isBrowsable: opts.isBrowsable ?? true,
       localeCode: opts.locale,
@@ -561,7 +566,7 @@ module.exports = class Page extends Model {
       })) {
         patch.scripts = {
           ...patch.scripts ?? ogPage.scripts ?? {},
-          css: !_.isEmpty(opts.patch.scriptCss) ? new CleanCSS({ inline: false }).minify(opts.patch.scriptCss).styles : ''
+          css: !isEmpty(opts.patch.scriptCss) ? new CleanCSS({ inline: false }).minify(opts.patch.scriptCss).styles : ''
         }
         historyData.affectedFields.push('scripts.css')
       }
@@ -698,7 +703,7 @@ module.exports = class Page extends Model {
 
     // -> Check content type
     const sourceContentType = ogPage.contentType
-    const targetContentType = _.get(_.find(WIKI.data.editors, ['key', opts.editor]), `contentType`, 'text')
+    const targetContentType = get(find(WIKI.data.editors, ['key', opts.editor]), `contentType`, 'text')
     const shouldConvert = sourceContentType !== targetContentType
     let convertedContent = null
 
@@ -846,7 +851,7 @@ module.exports = class Page extends Model {
    */
   static async movePage(opts) {
     let page
-    if (_.has(opts, 'id')) {
+    if (has(opts, 'id')) {
       page = await WIKI.db.pages.query().findById(opts.id)
     } else {
       page = await WIKI.db.pages.query().findOne({
@@ -904,7 +909,7 @@ module.exports = class Page extends Model {
       versionDate: page.updatedAt
     })
 
-    const destinationHash = pageHelper.generateHash({ path: opts.destinationPath, locale: opts.destinationLocale })
+    const destinationHash = generateHash({ path: opts.destinationPath, locale: opts.destinationLocale })
 
     // -> Move page
     const destinationTitle = (page.title === page.path ? opts.destinationPath : page.title)
@@ -970,7 +975,7 @@ module.exports = class Page extends Model {
    * @returns {Promise} Promise with no value
    */
   static async deletePage(opts) {
-    const page = await WIKI.db.pages.getPageFromDb(_.has(opts, 'id') ? opts.id : opts)
+    const page = await WIKI.db.pages.getPageFromDb(has(opts, 'id') ? opts.id : opts)
     if (!page) {
       throw new WIKI.Error.PageNotFound()
     }
@@ -1209,7 +1214,7 @@ module.exports = class Page extends Model {
    */
   static async savePageToCache(page) {
     const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${page.hash}.bin`)
-    await fs.outputFile(cachePath, WIKI.db.pages.cacheSchema.encode({
+    await fse.outputFile(cachePath, WIKI.db.pages.cacheSchema.encode({
       id: page.id,
       authorId: page.authorId,
       authorName: page.authorName,
@@ -1219,17 +1224,17 @@ module.exports = class Page extends Model {
       description: page.description,
       editor: page.editor,
       extra: {
-        css: _.get(page, 'extra.css', ''),
-        js: _.get(page, 'extra.js', '')
+        css: get(page, 'extra.css', ''),
+        js: get(page, 'extra.js', '')
       },
       publishState: page.publishState ?? '',
       publishEndDate: page.publishEndDate ?? '',
       publishStartDate: page.publishStartDate ?? '',
       render: page.render,
       siteId: page.siteId,
-      tags: page.tags.map(t => _.pick(t, ['tag'])),
+      tags: page.tags.map(t => pick(t, ['tag'])),
       title: page.title,
-      toc: _.isString(page.toc) ? page.toc : JSON.stringify(page.toc),
+      toc: isString(page.toc) ? page.toc : JSON.stringify(page.toc),
       updatedAt: page.updatedAt.toISOString()
     }))
   }
@@ -1241,11 +1246,11 @@ module.exports = class Page extends Model {
    * @returns {Promise} Promise of the Page Model Instance
    */
   static async getPageFromCache(opts) {
-    const pageHash = pageHelper.generateHash({ path: opts.path, locale: opts.locale })
+    const pageHash = generateHash({ path: opts.path, locale: opts.locale })
     const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${pageHash}.bin`)
 
     try {
-      const pageBuffer = await fs.readFile(cachePath)
+      const pageBuffer = await fse.readFile(cachePath)
       let page = WIKI.db.pages.cacheSchema.decode(pageBuffer)
       return {
         ...page,
@@ -1268,14 +1273,14 @@ module.exports = class Page extends Model {
    * @returns {Promise} Promise with no value
    */
   static async deletePageFromCache(hash) {
-    return fs.remove(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${hash}.bin`))
+    return fse.remove(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${hash}.bin`))
   }
 
   /**
    * Flush the contents of the Cache
    */
   static async flushCache() {
-    return fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache`))
+    return fse.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache`))
   }
 
   /**

+ 24 - 24
server/models/renderers.js → server/models/renderers.mjs

@@ -1,15 +1,15 @@
-const Model = require('objection').Model
-const path = require('path')
-const fs = require('fs-extra')
-const _ = require('lodash')
-const yaml = require('js-yaml')
-const DepGraph = require('dependency-graph').DepGraph
-const commonHelper = require('../helpers/common')
+import { Model } from 'objection'
+import path from 'node:path'
+import fs from 'fs/promises'
+import { clone, filter, find, get, has, reverse, some, transform, union } from 'lodash-es'
+import yaml from 'js-yaml'
+import { DepGraph } from 'dependency-graph'
+import { parseModuleProps } from '../helpers/common.mjs'
 
 /**
  * Renderer model
  */
-module.exports = class Renderer extends Model {
+export class Renderer extends Model {
   static get tableName() { return 'renderers' }
 
   static get jsonSchema () {
@@ -42,7 +42,7 @@ module.exports = class Renderer extends Model {
         const def = await fs.readFile(path.join(WIKI.SERVERPATH, 'modules/rendering', dir, 'definition.yml'), 'utf8')
         const defParsed = yaml.load(def)
         defParsed.key = dir
-        defParsed.props = commonHelper.parseModuleProps(defParsed.props)
+        defParsed.props = parseModuleProps(defParsed.props)
         WIKI.data.renderers.push(defParsed)
         WIKI.logger.debug(`Loaded renderers module definition ${dir}: [ OK ]`)
       }
@@ -65,20 +65,20 @@ module.exports = class Renderer extends Model {
       const newRenderers = []
       let updatedRenderers = 0
       for (const renderer of WIKI.data.renderers) {
-        if (!_.some(dbRenderers, ['module', renderer.key])) {
+        if (!some(dbRenderers, ['module', renderer.key])) {
           newRenderers.push({
             module: renderer.key,
             isEnabled: renderer.enabledDefault ?? true,
-            config: _.transform(renderer.props, (result, value, key) => {
+            config: transform(renderer.props, (result, value, key) => {
               result[key] = value.default
               return result
             }, {})
           })
         } else {
-          const rendererConfig = _.get(_.find(dbRenderers, ['module', renderer.key]), 'config', {})
+          const rendererConfig = get(find(dbRenderers, ['module', renderer.key]), 'config', {})
           await WIKI.db.renderers.query().patch({
-            config: _.transform(renderer.props, (result, value, key) => {
-              if (!_.has(result, key)) {
+            config: transform(renderer.props, (result, value, key) => {
+              if (!has(result, key)) {
                 result[key] = value.default
               }
               return result
@@ -98,7 +98,7 @@ module.exports = class Renderer extends Model {
 
       // -> Delete removed Renderers
       for (const renderer of dbRenderers) {
-        if (!_.some(WIKI.data.renderers, ['key', renderer.module])) {
+        if (!some(WIKI.data.renderers, ['key', renderer.module])) {
           await WIKI.db.renderers.query().where('module', renderer.key).del()
           WIKI.logger.info(`Removed renderer ${renderer.key} because it is no longer present in the modules folder: [ OK ]`)
         }
@@ -113,7 +113,7 @@ module.exports = class Renderer extends Model {
     const renderersDb = await WIKI.db.renderers.query().where('isEnabled', true)
     if (renderersDb && renderersDb.length > 0) {
       const renderers = renderersDb.map(rdr => {
-        const renderer = _.find(WIKI.data.renderers, ['key', rdr.module])
+        const renderer = find(WIKI.data.renderers, ['key', rdr.module])
         return {
           ...renderer,
           config: rdr.config
@@ -121,8 +121,8 @@ module.exports = class Renderer extends Model {
       })
 
       // Build tree
-      const rawCores = _.filter(renderers, renderer => !_.has(renderer, 'dependsOn')).map(core => {
-        core.children = _.filter(renderers, ['dependsOn', core.key])
+      const rawCores = filter(renderers, renderer => !has(renderer, 'dependsOn')).map(core => {
+        core.children = filter(renderers, ['dependsOn', core.key])
         return core
       })
 
@@ -140,11 +140,11 @@ module.exports = class Renderer extends Model {
       })
 
       // Filter unused cores
-      let activeCoreKeys = _.filter(rawCores, ['input', contentType]).map(core => core.key)
-      _.clone(activeCoreKeys).map(coreKey => {
-        activeCoreKeys = _.union(activeCoreKeys, graph.dependenciesOf(coreKey))
+      let activeCoreKeys = filter(rawCores, ['input', contentType]).map(core => core.key)
+      clone(activeCoreKeys).map(coreKey => {
+        activeCoreKeys = union(activeCoreKeys, graph.dependenciesOf(coreKey))
       })
-      const activeCores = _.filter(rawCores, core => _.includes(activeCoreKeys, core.key))
+      const activeCores = filter(rawCores, core => activeCoreKeys.includes(core.key))
 
       // Rebuild dependency graph with active cores
       const graphActive = new DepGraph({ circular: true })
@@ -161,8 +161,8 @@ module.exports = class Renderer extends Model {
 
       // Reorder cores in reverse dependency order
       let orderedCores = []
-      _.reverse(graphActive.overallOrder()).map(coreKey => {
-        orderedCores.push(_.find(rawCores, ['key', coreKey]))
+      reverse(graphActive.overallOrder()).map(coreKey => {
+        orderedCores.push(find(rawCores, ['key', coreKey]))
       })
 
       return orderedCores

+ 5 - 5
server/models/settings.js → server/models/settings.mjs

@@ -1,10 +1,10 @@
-const Model = require('objection').Model
-const _ = require('lodash')
+import { Model } from 'objection'
+import { has, reduce, set } from 'lodash-es'
 
 /**
  * Settings model
  */
-module.exports = class Setting extends Model {
+export class Setting extends Model {
   static get tableName() { return 'settings' }
   static get idColumn() { return 'key' }
 
@@ -26,8 +26,8 @@ module.exports = class Setting extends Model {
   static async getConfig() {
     const settings = await WIKI.db.settings.query()
     if (settings.length > 0) {
-      return _.reduce(settings, (res, val, key) => {
-        _.set(res, val.key, (_.has(val.value, 'v')) ? val.value.v : val.value)
+      return reduce(settings, (res, val, key) => {
+        set(res, val.key, (has(val.value, 'v')) ? val.value.v : val.value)
         return res
       }, {})
     } else {

+ 5 - 7
server/models/sites.js → server/models/sites.mjs

@@ -1,12 +1,10 @@
-const Model = require('objection').Model
-const crypto = require('crypto')
-const pem2jwk = require('pem-jwk').pem2jwk
-const _ = require('lodash')
+import { Model } from 'objection'
+import { defaultsDeep, keyBy } from 'lodash-es'
 
 /**
  * Site model
  */
-module.exports = class Site extends Model {
+export class Site extends Model {
   static get tableName () { return 'sites' }
 
   static get jsonSchema () {
@@ -40,7 +38,7 @@ module.exports = class Site extends Model {
   static async reloadCache () {
     WIKI.logger.info('Reloading site configurations...')
     const sites = await WIKI.db.sites.query().orderBy('id')
-    WIKI.sites = _.keyBy(sites, 'id')
+    WIKI.sites = keyBy(sites, 'id')
     WIKI.sitesMappings = {}
     for (const site of sites) {
       WIKI.sitesMappings[site.hostname] = site.id
@@ -52,7 +50,7 @@ module.exports = class Site extends Model {
     const newSite = await WIKI.db.sites.query().insertAndFetch({
       hostname,
       isEnabled: true,
-      config: _.defaultsDeep(config, {
+      config: defaultsDeep(config, {
         title: 'My Wiki Site',
         description: '',
         company: '',

+ 11 - 12
server/models/storage.js → server/models/storage.mjs

@@ -1,14 +1,13 @@
-const Model = require('objection').Model
-const path = require('path')
-const fs = require('fs-extra')
-const _ = require('lodash')
-const yaml = require('js-yaml')
-const commonHelper = require('../helpers/common')
+import { Model } from 'objection'
+import path from 'node:path'
+import fs from 'node:fs/promises'
+import { capitalize, find, has, hasIn, uniq } from 'lodash-es'
+import yaml from 'js-yaml'
 
 /**
  * Storage model
  */
-module.exports = class Storage extends Model {
+export class Storage extends Model {
   static get tableName() { return 'storage' }
   static get idColumn() { return 'id' }
 
@@ -67,7 +66,7 @@ module.exports = class Storage extends Model {
  * Ensure a storage module is loaded
  */
   static async ensureModule (moduleName) {
-    if (!_.has(WIKI.storage.modules, moduleName)) {
+    if (!has(WIKI.storage.modules, moduleName)) {
       try {
         WIKI.storage.modules[moduleName] = require(`../modules/storage/${moduleName}/storage`)
         WIKI.logger.debug(`Activated storage module ${moduleName}: [ OK ]`)
@@ -87,7 +86,7 @@ module.exports = class Storage extends Model {
    */
   static async initTargets () {
     const dbTargets = await WIKI.db.storage.query().where('isEnabled', true)
-    const activeModules = _.uniq(dbTargets.map(t => t.module))
+    const activeModules = uniq(dbTargets.map(t => t.module))
     try {
       // -> Stop and delete existing jobs
       // const prevjobs = _.remove(WIKI.scheduler.jobs, job => job.name === 'sync-storage')
@@ -168,7 +167,7 @@ module.exports = class Storage extends Model {
   static async assetEvent({ event, asset }) {
     try {
       for (let target of this.targets) {
-        await target.fn[`asset${_.capitalize(event)}`](asset)
+        await target.fn[`asset${capitalize(event)}`](asset)
       }
     } catch (err) {
       WIKI.logger.warn(err)
@@ -195,9 +194,9 @@ module.exports = class Storage extends Model {
 
   static async executeAction(targetKey, handler) {
     try {
-      const target = _.find(this.targets, ['key', targetKey])
+      const target = find(this.targets, ['key', targetKey])
       if (target) {
-        if (_.hasIn(target.fn, handler)) {
+        if (hasIn(target.fn, handler)) {
           await target.fn[handler]()
         } else {
           throw new Error('Invalid Handler for Storage Target')

+ 13 - 11
server/models/tags.js → server/models/tags.mjs

@@ -1,10 +1,12 @@
-const Model = require('objection').Model
-const _ = require('lodash')
+import { Model } from 'objection'
+import { concat, differenceBy, some, uniq } from 'lodash-es'
+
+import { Page } from './pages.mjs'
 
 /**
  * Tags model
  */
-module.exports = class Tag extends Model {
+export class Tag extends Model {
   static get tableName() { return 'tags' }
 
   static get jsonSchema () {
@@ -26,7 +28,7 @@ module.exports = class Tag extends Model {
     return {
       pages: {
         relation: Model.ManyToManyRelation,
-        modelClass: require('./pages'),
+        modelClass: Page,
         join: {
           from: 'tags.id',
           through: {
@@ -52,15 +54,15 @@ module.exports = class Tag extends Model {
 
     // Format tags
 
-    tags = _.uniq(tags.map(t => _.trim(t).toLowerCase()))
+    tags = uniq(tags.map(t => t.trim().toLowerCase()))
 
     // Create missing tags
 
-    const newTags = _.filter(tags, t => !_.some(existingTags, ['tag', t])).map(t => ({ tag: t }))
+    const newTags = tags.filter(t => !some(existingTags, ['tag', t])).map(t => ({ tag: t }))
     if (newTags.length > 0) {
       if (WIKI.config.db.type === 'postgres') {
         const createdTags = await WIKI.db.tags.query().insert(newTags)
-        existingTags = _.concat(existingTags, createdTags)
+        existingTags = concat(existingTags, createdTags)
       } else {
         for (const newTag of newTags) {
           const createdTag = await WIKI.db.tags.query().insert(newTag)
@@ -71,12 +73,12 @@ module.exports = class Tag extends Model {
 
     // Fetch current page tags
 
-    const targetTags = _.filter(existingTags, t => _.includes(tags, t.tag))
+    const targetTags = existingTags.filter(t => _.includes(tags, t.tag))
     const currentTags = await page.$relatedQuery('tags')
 
     // Tags to relate
 
-    const tagsToRelate = _.differenceBy(targetTags, currentTags, 'id')
+    const tagsToRelate = differenceBy(targetTags, currentTags, 'id')
     if (tagsToRelate.length > 0) {
       if (WIKI.config.db.type === 'postgres') {
         await page.$relatedQuery('tags').relate(tagsToRelate)
@@ -89,9 +91,9 @@ module.exports = class Tag extends Model {
 
     // Tags to unrelate
 
-    const tagsToUnrelate = _.differenceBy(currentTags, targetTags, 'id')
+    const tagsToUnrelate = differenceBy(currentTags, targetTags, 'id')
     if (tagsToUnrelate.length > 0) {
-      await page.$relatedQuery('tags').unrelate().whereIn('tags.id', _.map(tagsToUnrelate, 'id'))
+      await page.$relatedQuery('tags').unrelate().whereIn('tags.id', tagsToUnrelate.map(t => t.id))
     }
 
     page.tags = targetTags

+ 29 - 27
server/models/tree.js → server/models/tree.mjs

@@ -1,7 +1,9 @@
-const Model = require('objection').Model
-const _ = require('lodash')
+import { Model } from 'objection'
+import { differenceWith, dropRight, last, nth } from 'lodash-es'
+import { decodeTreePath, encodeTreePath, generateHash } from '../helpers/common.mjs'
 
-const commonHelper = require('../helpers/common')
+import { Locale } from './locales.mjs'
+import { Site } from './sites.mjs'
 
 const rePathName = /^[a-z0-9-]+$/
 const reTitle = /^[^<>"]+$/
@@ -9,7 +11,7 @@ const reTitle = /^[^<>"]+$/
 /**
  * Tree model
  */
-module.exports = class Tree extends Model {
+export class Tree extends Model {
   static get tableName() { return 'tree' }
 
   static get jsonSchema () {
@@ -37,7 +39,7 @@ module.exports = class Tree extends Model {
     return {
       locale: {
         relation: Model.BelongsToOneRelation,
-        modelClass: require('./locales'),
+        modelClass: Locale,
         join: {
           from: 'tree.localeCode',
           to: 'locales.code'
@@ -45,7 +47,7 @@ module.exports = class Tree extends Model {
       },
       site: {
         relation: Model.BelongsToOneRelation,
-        modelClass: require('./sites'),
+        modelClass: Site,
         join: {
           from: 'tree.siteId',
           to: 'sites.id'
@@ -82,11 +84,11 @@ module.exports = class Tree extends Model {
       return parent
     } else {
       // Get by path
-      const parentPath = commonHelper.encodeTreePath(path)
+      const parentPath = encodeTreePath(path)
       const parentPathParts = parentPath.split('.')
       const parentFilter = {
-        folderPath: _.dropRight(parentPathParts).join('.'),
-        fileName: _.last(parentPathParts)
+        folderPath: dropRight(parentPathParts).join('.'),
+        fileName: last(parentPathParts)
       }
       const parent = await WIKI.db.knex('tree').where({
         ...parentFilter,
@@ -133,7 +135,7 @@ module.exports = class Tree extends Model {
       folderPath: '',
       fileName: ''
     }
-    const folderPath = commonHelper.decodeTreePath(folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName)
+    const folderPath = decodeTreePath(folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName)
     const fullPath = folderPath ? `${folderPath}/${fileName}` : fileName
 
     WIKI.logger.debug(`Adding page ${fullPath} to tree...`)
@@ -144,7 +146,7 @@ module.exports = class Tree extends Model {
       fileName,
       type: 'page',
       title: title,
-      hash: commonHelper.generateHash(fullPath),
+      hash: generateHash(fullPath),
       localeCode: locale,
       siteId,
       meta
@@ -177,7 +179,7 @@ module.exports = class Tree extends Model {
       fileName: ''
     }
     const folderPath = folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName
-    const decodedFolderPath = commonHelper.decodeTreePath(folderPath)
+    const decodedFolderPath = decodeTreePath(folderPath)
     const fullPath = decodedFolderPath ? `${decodedFolderPath}/${fileName}` : fileName
 
     WIKI.logger.debug(`Adding asset ${fullPath} to tree...`)
@@ -188,7 +190,7 @@ module.exports = class Tree extends Model {
       fileName,
       type: 'asset',
       title: title,
-      hash: commonHelper.generateHash(fullPath),
+      hash: generateHash(fullPath),
       localeCode: locale,
       siteId,
       meta
@@ -219,12 +221,12 @@ module.exports = class Tree extends Model {
       throw new Error('ERR_INVALID_TITLE')
     }
 
-    parentPath = commonHelper.encodeTreePath(parentPath)
+    parentPath = encodeTreePath(parentPath)
     WIKI.logger.debug(`Creating new folder ${pathName}...`)
     const parentPathParts = parentPath.split('.')
     const parentFilter = {
-      folderPath: _.dropRight(parentPathParts).join('.'),
-      fileName: _.last(parentPathParts)
+      folderPath: dropRight(parentPathParts).join('.'),
+      fileName: last(parentPathParts)
     }
 
     // Get parent path
@@ -259,8 +261,8 @@ module.exports = class Tree extends Model {
         const parentPathParts = parentPath.split('.')
         for (let i = 1; i <= parentPathParts.length; i++) {
           const ancestor = {
-            folderPath: _.dropRight(parentPathParts, i).join('.'),
-            fileName: _.nth(parentPathParts, i * -1)
+            folderPath: dropRight(parentPathParts, i).join('.'),
+            fileName: nth(parentPathParts, i * -1)
           }
           expectedAncestors.push(ancestor)
           builder.orWhere({
@@ -269,14 +271,14 @@ module.exports = class Tree extends Model {
           })
         }
       })
-      for (const ancestor of _.differenceWith(expectedAncestors, existingAncestors, (expAnc, exsAnc) => expAnc.folderPath === exsAnc.folderPath && expAnc.fileName === exsAnc.fileName)) {
+      for (const ancestor of differenceWith(expectedAncestors, existingAncestors, (expAnc, exsAnc) => expAnc.folderPath === exsAnc.folderPath && expAnc.fileName === exsAnc.fileName)) {
         WIKI.logger.debug(`Creating missing parent folder ${ancestor.fileName} at path /${ancestor.folderPath}...`)
-        const newAncestorFullPath = ancestor.folderPath ? `${commonHelper.decodeTreePath(ancestor.folderPath)}/${ancestor.fileName}` : ancestor.fileName
+        const newAncestorFullPath = ancestor.folderPath ? `${decodeTreePath(ancestor.folderPath)}/${ancestor.fileName}` : ancestor.fileName
         const newAncestor = await WIKI.db.knex('tree').insert({
           ...ancestor,
           type: 'folder',
           title: ancestor.fileName,
-          hash: commonHelper.generateHash(newAncestorFullPath),
+          hash: generateHash(newAncestorFullPath),
           localeCode: locale,
           siteId: siteId,
           meta: {
@@ -292,13 +294,13 @@ module.exports = class Tree extends Model {
     }
 
     // Create folder
-    const fullPath = parentPath ? `${commonHelper.decodeTreePath(parentPath)}/${pathName}` : pathName
+    const fullPath = parentPath ? `${decodeTreePath(parentPath)}/${pathName}` : pathName
     const folder = await WIKI.db.knex('tree').insert({
       folderPath: parentPath,
       fileName: pathName,
       type: 'folder',
       title: title,
-      hash: commonHelper.generateHash(fullPath),
+      hash: generateHash(fullPath),
       localeCode: locale,
       siteId: siteId,
       meta: {
@@ -375,11 +377,11 @@ module.exports = class Tree extends Model {
       })
 
       // Rename the folder itself
-      const fullPath = folder.folderPath ? `${commonHelper.decodeTreePath(folder.folderPath)}/${pathName}` : pathName
+      const fullPath = folder.folderPath ? `${decodeTreePath(folder.folderPath)}/${pathName}` : pathName
       await WIKI.db.knex('tree').where('id', folder.id).update({
         fileName: pathName,
         title: title,
-        hash: commonHelper.generateHash(fullPath)
+        hash: generateHash(fullPath)
       })
     } else {
       // Update the folder title only
@@ -437,8 +439,8 @@ module.exports = class Tree extends Model {
     if (folder.folderPath) {
       const parentPathParts = folder.folderPath.split('.')
       const parent = await WIKI.db.knex('tree').where({
-        folderPath: _.dropRight(parentPathParts).join('.'),
-        fileName: _.last(parentPathParts)
+        folderPath: dropRight(parentPathParts).join('.'),
+        fileName: last(parentPathParts)
       }).first()
       await WIKI.db.knex('tree').where('id', parent.id).update({
         meta: {

+ 7 - 5
server/models/userKeys.js → server/models/userKeys.mjs

@@ -1,13 +1,15 @@
 /* global WIKI */
 
-const Model = require('objection').Model
-const { DateTime } = require('luxon')
-const { nanoid } = require('nanoid')
+import { Model } from 'objection'
+import { DateTime } from 'luxon'
+import { nanoid } from 'nanoid'
+
+import { User } from './users.mjs'
 
 /**
  * Users model
  */
-module.exports = class UserKey extends Model {
+export class UserKey extends Model {
   static get tableName() { return 'userKeys' }
 
   static get jsonSchema () {
@@ -29,7 +31,7 @@ module.exports = class UserKey extends Model {
     return {
       user: {
         relation: Model.BelongsToOneRelation,
-        modelClass: require('./users'),
+        modelClass: User,
         join: {
           from: 'userKeys.userId',
           to: 'users.id'

+ 67 - 64
server/models/users.js → server/models/users.mjs

@@ -1,18 +1,21 @@
 /* global WIKI */
 
-const _ = require('lodash')
-const tfa = require('node-2fa')
-const jwt = require('jsonwebtoken')
-const Model = require('objection').Model
-const validate = require('validate.js')
-const qr = require('qr-image')
+import { difference, find, first, flatten, flattenDeep, get, has, isArray, isEmpty, isNil, last, set, toString, truncate, uniq } from 'lodash-es'
+import tfa from 'node-2fa'
+import jwt from 'jsonwebtoken'
+import { Model } from 'objection'
+import validate from 'validate.js'
+import qr from 'qr-image'
+
+import { Group } from './groups.mjs'
+import { Locale } from './locales.mjs'
 
 const bcryptRegexp = /^\$2[ayb]\$[0-9]{2}\$[A-Za-z0-9./]{53}$/
 
 /**
  * Users model
  */
-module.exports = class User extends Model {
+export class User extends Model {
   static get tableName() { return 'users' }
 
   static get jsonSchema () {
@@ -42,7 +45,7 @@ module.exports = class User extends Model {
     return {
       groups: {
         relation: Model.ManyToManyRelation,
-        modelClass: require('./groups'),
+        modelClass: Group,
         join: {
           from: 'users.id',
           through: {
@@ -54,7 +57,7 @@ module.exports = class User extends Model {
       },
       locale: {
         relation: Model.BelongsToOneRelation,
-        modelClass: require('./locales'),
+        modelClass: Locale,
         join: {
           from: 'users.localeCode',
           to: 'locales.code'
@@ -113,15 +116,15 @@ module.exports = class User extends Model {
 
   verifyTFA(code) {
     let result = tfa.verifyToken(this.tfaSecret, code)
-    return (result && _.has(result, 'delta') && result.delta === 0)
+    return (result && has(result, 'delta') && result.delta === 0)
   }
 
   getPermissions () {
-    return _.uniq(_.flatten(_.map(this.groups, 'permissions')))
+    return uniq(flatten(this.groups.map(g => g.permissions)))
   }
 
   getGroups() {
-    return _.uniq(_.map(this.groups, 'id'))
+    return uniq(this.groups.map(g => g.id))
   }
 
   // ------------------------------------------------
@@ -135,32 +138,32 @@ module.exports = class User extends Model {
   }
 
   static async processProfile({ profile, providerKey }) {
-    const provider = _.get(WIKI.auth.strategies, providerKey, {})
-    provider.info = _.find(WIKI.data.authentication, ['key', provider.stategyKey])
+    const provider = get(WIKI.auth.strategies, providerKey, {})
+    provider.info = find(WIKI.data.authentication, ['key', provider.stategyKey])
 
     // Find existing user
     let user = await WIKI.db.users.query().findOne({
-      providerId: _.toString(profile.id),
+      providerId: toString(profile.id),
       providerKey
     })
 
     // Parse email
     let primaryEmail = ''
-    if (_.isArray(profile.emails)) {
-      const e = _.find(profile.emails, ['primary', true])
-      primaryEmail = (e) ? e.value : _.first(profile.emails).value
-    } else if (_.isArray(profile.email)) {
-      primaryEmail = _.first(_.flattenDeep([profile.email]))
-    } else if (_.isString(profile.email) && profile.email.length > 5) {
+    if (isArray(profile.emails)) {
+      const e = find(profile.emails, ['primary', true])
+      primaryEmail = (e) ? e.value : first(profile.emails).value
+    } else if (isArray(profile.email)) {
+      primaryEmail = first(flattenDeep([profile.email]))
+    } else if (isString(profile.email) && profile.email.length > 5) {
       primaryEmail = profile.email
-    } else if (_.isString(profile.mail) && profile.mail.length > 5) {
+    } else if (isString(profile.mail) && profile.mail.length > 5) {
       primaryEmail = profile.mail
     } else if (profile.user && profile.user.email && profile.user.email.length > 5) {
       primaryEmail = profile.user.email
     } else {
       throw new Error('Missing or invalid email address from profile.')
     }
-    primaryEmail = _.toLower(primaryEmail)
+    primaryEmail = primaryEmail.toLowerCase()
 
     // Find pending social user
     if (!user) {
@@ -171,16 +174,16 @@ module.exports = class User extends Model {
       })
       if (user) {
         user = await user.$query().patchAndFetch({
-          providerId: _.toString(profile.id)
+          providerId: toString(profile.id)
         })
       }
     }
 
     // Parse display name
     let displayName = ''
-    if (_.isString(profile.displayName) && profile.displayName.length > 0) {
+    if (isString(profile.displayName) && profile.displayName.length > 0) {
       displayName = profile.displayName
-    } else if (_.isString(profile.name) && profile.name.length > 0) {
+    } else if (isString(profile.name) && profile.name.length > 0) {
       displayName = profile.name
     } else {
       displayName = primaryEmail.split('@')[0]
@@ -191,7 +194,7 @@ module.exports = class User extends Model {
     if (profile.picture && Buffer.isBuffer(profile.picture)) {
       pictureUrl = 'internal'
     } else {
-      pictureUrl = _.truncate(_.get(profile, 'picture', _.get(user, 'pictureUrl', null)), {
+      pictureUrl = truncate(get(profile, 'picture', get(user, 'pictureUrl', null)), {
         length: 255,
         omission: ''
       })
@@ -222,9 +225,9 @@ module.exports = class User extends Model {
     // Self-registration
     if (provider.selfRegistration) {
       // Check if email domain is whitelisted
-      if (_.get(provider, 'domainWhitelist', []).length > 0) {
-        const emailDomain = _.last(primaryEmail.split('@'))
-        if (!_.includes(provider.domainWhitelist, emailDomain)) {
+      if (get(provider, 'domainWhitelist', []).length > 0) {
+        const emailDomain = last(primaryEmail.split('@'))
+        if (!provider.domainWhitelist.includes(emailDomain)) {
           throw new WIKI.Error.AuthRegistrationDomainUnauthorized()
         }
       }
@@ -232,7 +235,7 @@ module.exports = class User extends Model {
       // Create account
       user = await WIKI.db.users.query().insertAndFetch({
         providerKey: providerKey,
-        providerId: _.toString(profile.id),
+        providerId: toString(profile.id),
         email: primaryEmail,
         name: displayName,
         pictureUrl: pictureUrl,
@@ -263,19 +266,19 @@ module.exports = class User extends Model {
    * Login a user
    */
   static async login (opts, context) {
-    if (_.has(WIKI.auth.strategies, opts.strategy)) {
-      const selStrategy = _.get(WIKI.auth.strategies, opts.strategy)
+    if (has(WIKI.auth.strategies, opts.strategy)) {
+      const selStrategy = get(WIKI.auth.strategies, opts.strategy)
       if (!selStrategy.isEnabled) {
         throw new WIKI.Error.AuthProviderInvalid()
       }
 
-      const strInfo = _.find(WIKI.data.authentication, ['key', selStrategy.module])
+      const strInfo = find(WIKI.data.authentication, ['key', selStrategy.module])
 
       // Inject form user/pass
       if (strInfo.useForm) {
-        _.set(context.req, 'body.email', opts.username)
-        _.set(context.req, 'body.password', opts.password)
-        _.set(context.req.params, 'strategy', opts.strategy)
+        set(context.req, 'body.email', opts.username)
+        set(context.req, 'body.password', opts.password)
+        set(context.req.params, 'strategy', opts.strategy)
       }
 
       // Authenticate
@@ -312,7 +315,7 @@ module.exports = class User extends Model {
     let redirect = '/'
     if (user.groups && user.groups.length > 0) {
       for (const grp of user.groups) {
-        if (!_.isEmpty(grp.redirectOnLogin) && grp.redirectOnLogin !== '/') {
+        if (!isEmpty(grp.redirectOnLogin) && grp.redirectOnLogin !== '/') {
           redirect = grp.redirectOnLogin
           break
         }
@@ -391,7 +394,7 @@ module.exports = class User extends Model {
    * Generate a new token for a user
    */
   static async refreshToken(user, provider) {
-    if (_.isString(user)) {
+    if (isString(user)) {
       user = await WIKI.db.users.query().findById(user).withGraphFetched('groups').modifyGraph('groups', builder => {
         builder.select('groups.id', 'permissions')
       })
@@ -403,7 +406,7 @@ module.exports = class User extends Model {
         WIKI.logger.warn(`Failed to refresh token for user ${user}: Inactive.`)
         throw new WIKI.Error.AuthAccountBanned()
       }
-    } else if (_.isNil(user.groups)) {
+    } else if (isNil(user.groups)) {
       user.groups = await user.$relatedQuery('groups').select('groups.id', 'permissions')
     }
 
@@ -523,7 +526,7 @@ module.exports = class User extends Model {
    */
   static async createNewUser ({ providerKey, email, passwordRaw, name, groups, mustChangePassword, sendWelcomeEmail }) {
     // Input sanitization
-    email = _.toLower(email)
+    email = email.toLowerCase()
 
     // Input validation
     let validation = null
@@ -643,7 +646,7 @@ module.exports = class User extends Model {
     const usr = await WIKI.db.users.query().findById(id)
     if (usr) {
       let usrData = {}
-      if (!_.isEmpty(email) && email !== usr.email) {
+      if (!isEmpty(email) && email !== usr.email) {
         const dupUsr = await WIKI.db.users.query().select('id').where({
           email,
           providerKey: usr.providerKey
@@ -651,44 +654,44 @@ module.exports = class User extends Model {
         if (dupUsr) {
           throw new WIKI.Error.AuthAccountAlreadyExists()
         }
-        usrData.email = _.toLower(email)
+        usrData.email = email.toLowerCase()
       }
-      if (!_.isEmpty(name) && name !== usr.name) {
-        usrData.name = _.trim(name)
+      if (!isEmpty(name) && name !== usr.name) {
+        usrData.name = name.trim()
       }
-      if (!_.isEmpty(newPassword)) {
+      if (!isEmpty(newPassword)) {
         if (newPassword.length < 6) {
           throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!')
         }
         usrData.password = newPassword
       }
-      if (_.isArray(groups)) {
+      if (isArray(groups)) {
         const usrGroupsRaw = await usr.$relatedQuery('groups')
-        const usrGroups = _.map(usrGroupsRaw, 'id')
+        const usrGroups = usrGroupsRaw.map(g => g.id)
         // Relate added groups
-        const addUsrGroups = _.difference(groups, usrGroups)
+        const addUsrGroups = difference(groups, usrGroups)
         for (const grp of addUsrGroups) {
           await usr.$relatedQuery('groups').relate(grp)
         }
         // Unrelate removed groups
-        const remUsrGroups = _.difference(usrGroups, groups)
+        const remUsrGroups = difference(usrGroups, groups)
         for (const grp of remUsrGroups) {
           await usr.$relatedQuery('groups').unrelate().where('groupId', grp)
         }
       }
-      if (!_.isEmpty(location) && location !== usr.location) {
-        usrData.location = _.trim(location)
+      if (!isEmpty(location) && location !== usr.location) {
+        usrData.location = location.trim()
       }
-      if (!_.isEmpty(jobTitle) && jobTitle !== usr.jobTitle) {
-        usrData.jobTitle = _.trim(jobTitle)
+      if (!isEmpty(jobTitle) && jobTitle !== usr.jobTitle) {
+        usrData.jobTitle = jobTitle.trim()
       }
-      if (!_.isEmpty(timezone) && timezone !== usr.timezone) {
+      if (!isEmpty(timezone) && timezone !== usr.timezone) {
         usrData.timezone = timezone
       }
-      if (!_.isNil(dateFormat) && dateFormat !== usr.dateFormat) {
+      if (!isNil(dateFormat) && dateFormat !== usr.dateFormat) {
         usrData.dateFormat = dateFormat
       }
-      if (!_.isNil(appearance) && appearance !== usr.appearance) {
+      if (!isNil(appearance) && appearance !== usr.appearance) {
         usrData.appearance = appearance
       }
       await WIKI.db.users.query().patch(usrData).findById(id)
@@ -729,7 +732,7 @@ module.exports = class User extends Model {
     // Check if self-registration is enabled
     if (localStrg.selfRegistration || bypassChecks) {
       // Input sanitization
-      email = _.toLower(email)
+      email = email.toLowerCase()
 
       // Input validation
       const validation = validate({
@@ -766,9 +769,9 @@ module.exports = class User extends Model {
       }
 
       // Check if email domain is whitelisted
-      if (_.get(localStrg, 'domainWhitelist.v', []).length > 0 && !bypassChecks) {
-        const emailDomain = _.last(email.split('@'))
-        if (!_.includes(localStrg.domainWhitelist.v, emailDomain)) {
+      if (get(localStrg, 'domainWhitelist.v', []).length > 0 && !bypassChecks) {
+        const emailDomain = last(email.split('@'))
+        if (!localStrg.domainWhitelist.v.includes(emailDomain)) {
           throw new WIKI.Error.AuthRegistrationDomainUnauthorized()
         }
       }
@@ -790,7 +793,7 @@ module.exports = class User extends Model {
         })
 
         // Assign to group(s)
-        if (_.get(localStrg, 'autoEnrollGroups.v', []).length > 0) {
+        if (get(localStrg, 'autoEnrollGroups.v', []).length > 0) {
           await newUsr.$relatedQuery('groups').relate(localStrg.autoEnrollGroups.v)
         }
 
@@ -832,12 +835,12 @@ module.exports = class User extends Model {
     if (!context.req.user || context.req.user.id === WIKI.config.auth.guestUserId) {
       return '/'
     }
-    if (context.req.user.strategyId && _.has(WIKI.auth.strategies, context.req.user.strategyId)) {
+    if (context.req.user.strategyId && has(WIKI.auth.strategies, context.req.user.strategyId)) {
       const selStrategy = WIKI.auth.strategies[context.req.user.strategyId]
       if (!selStrategy.isEnabled) {
         throw new WIKI.Error.AuthProviderInvalid()
       }
-      const provider = _.find(WIKI.data.authentication, ['key', selStrategy.module])
+      const provider = find(WIKI.data.authentication, ['key', selStrategy.module])
       if (provider.logout) {
         return provider.logout(provider.config)
       }

文件差异内容过多而无法显示
+ 383 - 371
server/package-lock.json


+ 71 - 74
server/package.json

@@ -3,12 +3,13 @@
   "version": "3.0.0",
   "releaseDate": "2023-01-01T01:01:01.000Z",
   "description": "The most powerful and extensible open source Wiki software",
-  "main": "index.js",
+  "main": "index.mjs",
+  "type": "module",
   "private": true,
   "dev": true,
   "scripts": {
     "start": "cd .. && node server",
-    "dev": "cd .. && nodemon server --watch server --ext js,json,graphql,gql"
+    "dev": "cd .. && nodemon server --watch server --ext mjs,js,json,graphql,gql"
   },
   "repository": {
     "type": "git",
@@ -33,94 +34,91 @@
     "node": ">=18.0"
   },
   "dependencies": {
-    "@azure/storage-blob": "12.11.0",
+    "@azure/storage-blob": "12.13.0",
     "@exlinc/keycloak-passport": "1.0.2",
-    "@graphql-tools/schema": "8.3.7",
-    "@graphql-tools/utils": "8.6.6",
-    "@joplin/turndown-plugin-gfm": "1.0.44",
+    "@graphql-tools/schema": "9.0.17",
+    "@graphql-tools/utils": "9.2.1",
+    "@joplin/turndown-plugin-gfm": "1.0.47",
     "@root/csr": "0.8.1",
     "@root/keypairs": "0.10.3",
     "@root/pem": "1.0.4",
     "acme": "3.0.3",
-    "akismet-api": "5.3.0",
+    "akismet-api": "6.0.0",
     "apollo-fetch": "0.7.0",
     "apollo-server": "3.6.7",
     "apollo-server-express": "3.6.7",
     "auto-load": "3.0.4",
-    "aws-sdk": "2.1208.0",
+    "aws-sdk": "2.1353.0",
     "bcryptjs-then": "1.0.1",
-    "body-parser": "1.20.0",
-    "chalk": "4.1.2",
+    "body-parser": "1.20.2",
+    "chalk": "5.2.0",
     "cheerio": "1.0.0-rc.12",
     "chokidar": "3.5.3",
     "chromium-pickle-js": "0.2.0",
-    "clean-css": "4.2.3",
+    "clean-css": "5.3.2",
     "command-exists": "1.2.9",
     "compression": "1.7.4",
-    "connect-session-knex": "3.0.0",
+    "connect-session-knex": "3.0.1",
     "cookie-parser": "1.4.6",
     "cors": "2.8.5",
-    "cron-parser": "4.6.0",
+    "cron-parser": "4.8.1",
     "cuint": "0.2.2",
     "custom-error-instance": "2.1.2",
-    "dependency-graph": "0.9.0",
-    "diff": "4.0.2",
-    "diff2html": "3.1.14",
-    "dompurify": "2.4.0",
+    "dependency-graph": "0.11.0",
+    "diff": "5.1.0",
+    "diff2html": "3.4.34",
+    "dompurify": "3.0.1",
     "dotize": "0.3.0",
-    "emoji-regex": "10.1.0",
-    "eventemitter2": "6.4.7",
-    "express": "4.18.1",
+    "emoji-regex": "10.2.1",
+    "eventemitter2": "6.4.9",
+    "express": "4.18.2",
     "express-brute": "1.0.1",
     "express-session": "1.17.3",
-    "file-type": "15.0.1",
-    "filesize": "6.1.0",
-    "fs-extra": "9.0.1",
+    "file-type": "18.2.1",
+    "filesize": "10.0.7",
+    "fs-extra": "11.1.1",
     "getos": "3.2.1",
-    "graphql": "16.3.0",
+    "graphql": "16.6.0",
     "graphql-list-fields": "2.0.2",
-    "graphql-rate-limit-directive": "2.0.2",
-    "graphql-tools": "8.2.5",
-    "graphql-upload": "13.0.0",
+    "graphql-rate-limit-directive": "2.0.3",
+    "graphql-tools": "8.3.19",
+    "graphql-upload": "16.0.2",
     "he": "1.2.0",
-    "highlight.js": "10.3.1",
-    "i18next": "19.8.3",
+    "highlight.js": "11.7.0",
+    "i18next": "22.4.14",
     "i18next-node-fs-backend": "2.1.3",
-    "image-size": "0.9.2",
-    "js-base64": "3.7.2",
+    "image-size": "1.0.2",
+    "js-base64": "3.7.5",
     "js-binary": "1.2.0",
     "js-yaml": "4.1.0",
-    "jsdom": "16.4.0",
-    "jsonwebtoken": "8.5.1",
-    "katex": "0.12.0",
-    "klaw": "4.0.1",
-    "knex": "2.3.0",
+    "jsdom": "21.1.1",
+    "jsonwebtoken": "9.0.0",
+    "katex": "0.16.4",
+    "klaw": "4.1.0",
+    "knex": "2.4.2",
     "lodash": "4.17.21",
-    "luxon": "2.3.1",
-    "markdown-it": "11.0.1",
+    "lodash-es": "4.17.21",
+    "luxon": "3.3.0",
+    "markdown-it": "13.0.1",
     "markdown-it-abbr": "1.0.4",
-    "markdown-it-attrs": "3.0.3",
-    "markdown-it-emoji": "1.4.0",
+    "markdown-it-emoji": "2.0.2",
     "markdown-it-expand-tabs": "1.0.13",
     "markdown-it-external-links": "0.0.6",
     "markdown-it-footnote": "3.0.3",
     "markdown-it-imsize": "2.0.1",
     "markdown-it-mark": "3.0.1",
     "markdown-it-mathjax": "2.0.0",
-    "markdown-it-multimd-table": "4.0.3",
     "markdown-it-sub": "1.0.0",
     "markdown-it-sup": "1.0.0",
     "markdown-it-task-lists": "2.1.1",
-    "mathjax": "3.1.2",
+    "mathjax": "3.2.2",
     "mime-types": "2.1.35",
-    "moment": "2.29.2",
-    "moment-timezone": "0.5.31",
     "ms": "2.1.3",
     "multer": "1.4.4",
-    "nanoid": "3.3.2",
-    "node-2fa": "1.1.2",
+    "nanoid": "4.0.2",
+    "node-2fa": "2.0.3",
     "node-cache": "5.1.2",
-    "nodemailer": "6.7.8",
+    "nodemailer": "6.9.1",
     "objection": "3.0.1",
     "passport": "0.6.0",
     "passport-auth0": "1.4.3",
@@ -132,60 +130,59 @@
     "passport-github2": "0.1.12",
     "passport-gitlab2": "5.0.0",
     "passport-google-oauth20": "2.0.0",
-    "passport-jwt": "4.0.0",
+    "passport-jwt": "4.0.1",
     "passport-ldapauth": "3.0.1",
     "passport-local": "1.0.0",
     "passport-microsoft": "1.0.0",
-    "passport-oauth2": "1.6.1",
+    "passport-oauth2": "1.7.0",
     "passport-okta-oauth": "0.0.1",
     "passport-openidconnect": "0.1.1",
     "passport-saml": "3.2.1",
     "passport-slack-oauth2": "1.1.1",
     "passport-twitch-strategy": "2.2.0",
     "pem-jwk": "2.0.0",
-    "pg": "8.8.0",
+    "pg": "8.10.0",
     "pg-hstore": "2.3.4",
-    "pg-pubsub": "0.8.0",
-    "pg-query-stream": "4.2.4",
-    "pg-tsquery": "8.4.0",
-    "poolifier": "2.2.0",
+    "pg-pubsub": "0.8.1",
+    "pg-query-stream": "4.4.0",
+    "pg-tsquery": "8.4.1",
+    "poolifier": "2.4.4",
     "pug": "3.0.2",
-    "punycode": "2.1.1",
-    "puppeteer-core": "17.1.3",
+    "punycode": "2.3.0",
+    "puppeteer-core": "19.8.5",
     "qr-image": "3.2.0",
-    "rate-limiter-flexible": "2.3.8",
-    "remove-markdown": "0.3.0",
+    "rate-limiter-flexible": "2.4.1",
+    "remove-markdown": "0.5.0",
     "request": "2.88.2",
     "request-promise": "4.2.6",
     "safe-regex": "2.1.1",
     "sanitize-filename": "1.6.3",
     "scim-query-filter-parser": "2.0.4",
-    "semver": "7.3.7",
+    "semver": "7.3.8",
     "serve-favicon": "2.5.0",
-    "sharp": "0.31.0",
-    "simple-git": "2.21.0",
-    "socket.io": "4.5.2",
-    "ssh2": "1.9.0",
-    "ssh2-promise": "1.0.2",
+    "sharp": "0.32.0",
+    "simple-git": "3.17.0",
+    "socket.io": "4.6.1",
+    "ssh2": "1.11.0",
+    "ssh2-promise": "1.0.3",
     "striptags": "3.2.0",
     "tar-fs": "2.1.1",
-    "turndown": "7.1.1",
-    "twemoji": "13.1.0",
+    "turndown": "7.1.2",
     "uslug": "1.0.4",
-    "uuid": "8.3.2",
+    "uuid": "9.0.0",
     "validate.js": "0.13.1",
     "xss": "1.0.14",
-    "yargs": "16.1.0"
+    "yargs": "17.7.1"
   },
   "devDependencies": {
-    "eslint": "7.12.0",
+    "eslint": "8.38.0",
     "eslint-config-requarks": "1.0.7",
-    "eslint-config-standard": "15.0.0",
-    "eslint-plugin-import": "2.22.1",
+    "eslint-config-standard": "17.0.0",
+    "eslint-plugin-import": "2.27.5",
     "eslint-plugin-node": "11.1.0",
-    "eslint-plugin-promise": "4.2.1",
-    "eslint-plugin-standard": "4.0.2",
-    "nodemon": "2.0.15"
+    "eslint-plugin-promise": "6.1.1",
+    "eslint-plugin-standard": "4.1.0",
+    "nodemon": "2.0.22"
   },
   "collective": {
     "type": "opencollective",

+ 1 - 1
server/tasks/simple/check-version.js → server/tasks/simple/check-version.mjs

@@ -1,4 +1,4 @@
-module.exports = async (payload) => {
+export async function task (payload) {
   WIKI.logger.info('Checking for latest version...')
 
   try {

+ 2 - 2
server/tasks/simple/clean-job-history.js → server/tasks/simple/clean-job-history.mjs

@@ -1,6 +1,6 @@
-const { DateTime } = require('luxon')
+import { DateTime } from 'luxon'
 
-module.exports = async (payload) => {
+export async function task (payload) {
   WIKI.logger.info('Cleaning scheduler job history...')
 
   try {

+ 1 - 1
server/tasks/simple/update-locales.js → server/tasks/simple/update-locales.mjs

@@ -1,4 +1,4 @@
-module.exports = async (payload) => {
+export async function task (payload) {
   WIKI.logger.info('Fetching latest localization data...')
 
   try {

+ 8 - 8
server/tasks/workers/purge-uploads.js → server/tasks/workers/purge-uploads.mjs

@@ -1,20 +1,20 @@
-const path = require('node:path')
-const fs = require('fs-extra')
-const { DateTime } = require('luxon')
+import path from 'node:path'
+import fse from 'fs-extra'
+import { DateTime } from 'luxon'
 
-module.exports = async ({ payload }) => {
+export async function task ({ payload }) {
   WIKI.logger.info('Purging orphaned upload files...')
 
   try {
     const uplTempPath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, 'uploads')
-    await fs.ensureDir(uplTempPath)
-    const ls = await fs.readdir(uplTempPath)
+    await fse.ensureDir(uplTempPath)
+    const ls = await fse.readdir(uplTempPath)
     const fifteenAgo = DateTime.now().minus({ minutes: 15 })
 
     for (const f of ls) {
-      const stat = fs.stat(path.join(uplTempPath, f))
+      const stat = fse.stat(path.join(uplTempPath, f))
       if ((await stat).isFile && stat.ctime < fifteenAgo) {
-        await fs.unlink(path.join(uplTempPath, f))
+        await fse.unlink(path.join(uplTempPath, f))
       }
     }
 

+ 9 - 9
server/tasks/workers/render-page.js → server/tasks/workers/render-page.mjs

@@ -1,7 +1,7 @@
-const _ = require('lodash')
-const cheerio = require('cheerio')
+import { get, has, isEmpty, reduce, times, toSafeInteger } from 'lodash-es'
+import cheerio from 'cheerio'
 
-module.exports = async ({ payload }) => {
+export async function task ({ payload }) {
   WIKI.logger.info(`Rendering page ${payload.id}...`)
 
   try {
@@ -20,7 +20,7 @@ module.exports = async ({ payload }) => {
 
     let output = page.render
 
-    if (_.isEmpty(page.content)) {
+    if (isEmpty(page.content)) {
       WIKI.logger.warn(`Failed to render page ID ${payload.id} because content was empty: [ FAILED ]`)
     }
 
@@ -41,11 +41,11 @@ module.exports = async ({ payload }) => {
     let toc = { root: [] }
 
     $('h1,h2,h3,h4,h5,h6').each((idx, el) => {
-      const depth = _.toSafeInteger(el.name.substring(1)) - (isStrict ? 1 : 2)
+      const depth = toSafeInteger(el.name.substring(1)) - (isStrict ? 1 : 2)
       let leafPathError = false
 
-      const leafPath = _.reduce(_.times(depth), (curPath, curIdx) => {
-        if (_.has(toc, curPath)) {
+      const leafPath = reduce(times(depth), (curPath, curIdx) => {
+        if (has(toc, curPath)) {
           const lastLeafIdx = _.get(toc, curPath).length - 1
           if (lastLeafIdx >= 0) {
             curPath = `${curPath}[${lastLeafIdx}].children`
@@ -61,8 +61,8 @@ module.exports = async ({ payload }) => {
       const leafSlug = $('.toc-anchor', el).first().attr('href')
       $('.toc-anchor', el).remove()
 
-      _.get(toc, leafPath).push({
-        label: _.trim($(el).text()),
+      get(toc, leafPath).push({
+        label: $(el).text().trim(),
         key: leafSlug.substring(1),
         children: []
       })

+ 75 - 34
server/web.js → server/web.mjs

@@ -1,30 +1,31 @@
-const autoload = require('auto-load')
-const bodyParser = require('body-parser')
-const compression = require('compression')
-const cookieParser = require('cookie-parser')
-const cors = require('cors')
-const express = require('express')
-const session = require('express-session')
-const KnexSessionStore = require('connect-session-knex')(session)
-const favicon = require('serve-favicon')
-const path = require('path')
-const _ = require('lodash')
-
-module.exports = async () => {
+import bodyParser from 'body-parser'
+import compression from 'compression'
+import cookieParser from 'cookie-parser'
+import cors from 'cors'
+import express from 'express'
+import session from 'express-session'
+import KnexSessionStore from 'connect-session-knex'
+import favicon from 'serve-favicon'
+import path from 'node:path'
+import { set } from 'lodash-es'
+
+import auth from './core/auth.mjs'
+import mail from './core/mail.mjs'
+import system from './core/system.mjs'
+
+import ctrlAuth from './controllers/auth.mjs'
+import ctrlCommon from './controllers/common.mjs'
+import ctrlSsl from './controllers/ssl.mjs'
+import ctrlWs from './controllers/ws.mjs'
+
+export async function init () {
   // ----------------------------------------
   // Load core modules
   // ----------------------------------------
 
-  WIKI.auth = require('./core/auth').init()
-  WIKI.mail = require('./core/mail').init()
-  WIKI.system = require('./core/system').init()
-
-  // ----------------------------------------
-  // Load middlewares
-  // ----------------------------------------
-
-  const mw = autoload(path.join(WIKI.SERVERPATH, '/middlewares'))
-  const ctrl = autoload(path.join(WIKI.SERVERPATH, '/controllers'))
+  WIKI.auth = auth.init()
+  WIKI.mail = mail.init()
+  WIKI.system = system.init()
 
   // ----------------------------------------
   // Define Express App
@@ -41,24 +42,57 @@ module.exports = async () => {
   const useHTTPS = WIKI.config.ssl.enabled === true || WIKI.config.ssl.enabled === 'true' || WIKI.config.ssl.enabled === 1 || WIKI.config.ssl.enabled === '1'
 
   await WIKI.servers.initHTTP()
-
   if (useHTTPS) {
     await WIKI.servers.initHTTPS()
   }
-
   await WIKI.servers.initWebSocket()
 
   // ----------------------------------------
   // Attach WebSocket Server
   // ----------------------------------------
 
-  ctrl.ws()
+  ctrlWs()
 
   // ----------------------------------------
   // Security
   // ----------------------------------------
 
-  app.use(mw.security)
+  app.use((req, res, next) => {
+    // -> Disable X-Powered-By
+    req.app.disable('x-powered-by')
+
+    // -> Disable Frame Embedding
+    if (WIKI.config.security.securityIframe) {
+      res.set('X-Frame-Options', 'deny')
+    }
+
+    // -> Re-enable XSS Fitler if disabled
+    res.set('X-XSS-Protection', '1; mode=block')
+
+    // -> Disable MIME-sniffing
+    res.set('X-Content-Type-Options', 'nosniff')
+
+    // -> Disable IE Compatibility Mode
+    res.set('X-UA-Compatible', 'IE=edge')
+
+    // -> Disables referrer header when navigating to a different origin
+    if (WIKI.config.security.securityReferrerPolicy) {
+      res.set('Referrer-Policy', 'same-origin')
+    }
+
+    // -> Enforce HSTS
+    if (WIKI.config.security.securityHSTS) {
+      res.set('Strict-Transport-Security', `max-age=${WIKI.config.security.securityHSTSDuration}; includeSubDomains`)
+    }
+
+    // -> Prevent Open Redirect from user provided URL
+    if (WIKI.config.security.securityOpenRedirect) {
+      // Strips out all repeating / character in the provided URL
+      req.url = req.url.replace(/(\/)(?=\/*\1)/g, '')
+    }
+
+    next()
+  })
   app.use(cors({ origin: false }))
   app.options('*', cors({ origin: false }))
   if (WIKI.config.security.securityTrustProxy) {
@@ -86,7 +120,7 @@ module.exports = async () => {
   // SSL Handlers
   // ----------------------------------------
 
-  app.use('/', ctrl.ssl)
+  app.use('/', ctrlSsl())
 
   // ----------------------------------------
   // Passport Authentication
@@ -97,7 +131,7 @@ module.exports = async () => {
     secret: WIKI.config.auth.secret,
     resave: false,
     saveUninitialized: false,
-    store: new KnexSessionStore({
+    store: new KnexSessionStore(session)({
       knex: WIKI.db.knex
     })
   }))
@@ -115,7 +149,15 @@ module.exports = async () => {
   // SEO
   // ----------------------------------------
 
-  app.use(mw.seo)
+  app.use((req, res, next) => {
+    if (req.path.length > 1 && req.path.endsWith('/')) {
+      let query = req.url.slice(req.path.length) || ''
+      res.redirect(301, req.path.slice(0, -1) + query)
+    } else {
+      set(res.locals, 'pageMeta.url', `${WIKI.config.host}${req.path}`)
+      next()
+    }
+  })
 
   // ----------------------------------------
   // View Engine Setup
@@ -177,9 +219,8 @@ module.exports = async () => {
     next()
   })
 
-  app.use('/', ctrl.auth)
-  app.use('/', ctrl.upload)
-  app.use('/', ctrl.common)
+  app.use('/', ctrlAuth())
+  app.use('/', ctrlCommon())
 
   // ----------------------------------------
   // Error handling
@@ -202,7 +243,7 @@ module.exports = async () => {
       })
     } else {
       res.status(err.status || 500)
-      _.set(res.locals, 'pageMeta.title', 'Error')
+      set(res.locals, 'pageMeta.title', 'Error')
       res.render('error', {
         message: err.message,
         error: WIKI.IS_DEBUG ? err : {}

+ 6 - 6
server/worker.js

@@ -1,12 +1,12 @@
-const { ThreadWorker } = require('poolifier')
-const { kebabCase } = require('lodash')
-const path = require('node:path')
+import { ThreadWorker } from 'poolifier'
+import { kebabCase } from 'lodash-es'
+import path from 'node:path'
 
 // ----------------------------------------
 // Init Minimal Core
 // ----------------------------------------
 
-let WIKI = {
+const WIKI = {
   IS_DEBUG: process.env.NODE_ENV === 'development',
   ROOTPATH: process.cwd(),
   INSTANCE_ID: 'worker',
@@ -39,9 +39,9 @@ WIKI.logger = require('./core/logger').init()
 // Execute Task
 // ----------------------------------------
 
-module.exports = new ThreadWorker(async (job) => {
+export default new ThreadWorker(async (job) => {
   WIKI.INSTANCE_ID = job.INSTANCE_ID
-  const task = require(`./tasks/workers/${kebabCase(job.task)}.js`)
+  const task = (await import(`./tasks/workers/${kebabCase(job.task)}.mjs`)).task
   await task(job)
   return true
 }, { async: true })

+ 175 - 175
ux/package-lock.json

@@ -11,32 +11,32 @@
         "@apollo/client": "3.7.11",
         "@lezer/common": "1.0.2",
         "@mdi/font": "7.2.96",
-        "@quasar/extras": "1.16.1",
-        "@tiptap/core": "2.0.1",
-        "@tiptap/extension-code-block": "2.0.1",
-        "@tiptap/extension-code-block-lowlight": "2.0.1",
-        "@tiptap/extension-color": "2.0.1",
-        "@tiptap/extension-dropcursor": "2.0.1",
-        "@tiptap/extension-font-family": "2.0.1",
-        "@tiptap/extension-gapcursor": "2.0.1",
-        "@tiptap/extension-hard-break": "2.0.1",
-        "@tiptap/extension-highlight": "2.0.1",
-        "@tiptap/extension-history": "2.0.1",
-        "@tiptap/extension-image": "2.0.1",
-        "@tiptap/extension-mention": "2.0.1",
-        "@tiptap/extension-placeholder": "2.0.1",
-        "@tiptap/extension-table": "2.0.1",
-        "@tiptap/extension-table-cell": "2.0.1",
-        "@tiptap/extension-table-header": "2.0.1",
-        "@tiptap/extension-table-row": "2.0.1",
-        "@tiptap/extension-task-item": "2.0.1",
-        "@tiptap/extension-task-list": "2.0.1",
-        "@tiptap/extension-text-align": "2.0.1",
-        "@tiptap/extension-text-style": "2.0.1",
-        "@tiptap/extension-typography": "2.0.1",
-        "@tiptap/pm": "2.0.1",
-        "@tiptap/starter-kit": "2.0.1",
-        "@tiptap/vue-3": "2.0.1",
+        "@quasar/extras": "1.16.2",
+        "@tiptap/core": "2.0.2",
+        "@tiptap/extension-code-block": "2.0.2",
+        "@tiptap/extension-code-block-lowlight": "2.0.2",
+        "@tiptap/extension-color": "2.0.2",
+        "@tiptap/extension-dropcursor": "2.0.2",
+        "@tiptap/extension-font-family": "2.0.2",
+        "@tiptap/extension-gapcursor": "2.0.2",
+        "@tiptap/extension-hard-break": "2.0.2",
+        "@tiptap/extension-highlight": "2.0.2",
+        "@tiptap/extension-history": "2.0.2",
+        "@tiptap/extension-image": "2.0.2",
+        "@tiptap/extension-mention": "2.0.2",
+        "@tiptap/extension-placeholder": "2.0.2",
+        "@tiptap/extension-table": "2.0.2",
+        "@tiptap/extension-table-cell": "2.0.2",
+        "@tiptap/extension-table-header": "2.0.2",
+        "@tiptap/extension-table-row": "2.0.2",
+        "@tiptap/extension-task-item": "2.0.2",
+        "@tiptap/extension-task-list": "2.0.2",
+        "@tiptap/extension-text-align": "2.0.2",
+        "@tiptap/extension-text-style": "2.0.2",
+        "@tiptap/extension-typography": "2.0.2",
+        "@tiptap/pm": "2.0.2",
+        "@tiptap/starter-kit": "2.0.2",
+        "@tiptap/vue-3": "2.0.2",
         "apollo-upload-client": "17.0.0",
         "browser-fs-access": "0.33.0",
         "clipboard": "2.0.11",
@@ -78,7 +78,7 @@
         "prosemirror-transform": "1.7.1",
         "prosemirror-view": "1.30.2",
         "pug": "3.0.2",
-        "quasar": "2.11.9",
+        "quasar": "2.11.10",
         "slugify": "1.6.6",
         "socket.io-client": "4.6.1",
         "tabulator-tables": "5.4.4",
@@ -652,9 +652,9 @@
       }
     },
     "node_modules/@quasar/extras": {
-      "version": "1.16.1",
-      "resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.16.1.tgz",
-      "integrity": "sha512-bRnWSC469Qogw0ceDVd0yTQVBQjyhV6L10EMixXK1dpOs9tWGKRVgyyGZ5DEBkDgyn4BeRuV5s5e9VrQdQPsOg==",
+      "version": "1.16.2",
+      "resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.16.2.tgz",
+      "integrity": "sha512-spDc1DrwxGts0MjmOAJ11xpxJANhCI1vEadxaw89wRQJ/QfKd0HZrwN7uN1U15cRozGRkJpdbsnP4cVXpkPysA==",
       "funding": {
         "type": "github",
         "url": "https://donate.quasar.dev"
@@ -762,9 +762,9 @@
       "license": "MIT"
     },
     "node_modules/@tiptap/core": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.0.1.tgz",
-      "integrity": "sha512-IY5K17e1YdlJIykCt3NuOyqK/SHyGwk2X7eyLYfZJGJwqzuPuf3y8X1zZQQN53t5UVfsgqsF9RweVEee40o1Aw==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.0.2.tgz",
+      "integrity": "sha512-DBry6tpX7mYaTJkEDjVA4WmF8Kgthr275L0uIIOVdwW5nG5PAnOvREKyVOoMQnN3vR7CjtaCK+c3y+MCQhMA/g==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -774,9 +774,9 @@
       }
     },
     "node_modules/@tiptap/extension-blockquote": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.0.1.tgz",
-      "integrity": "sha512-j4n5iTeBc/YIoW83nZPoc1/fKeoA32tPaOH/quQdtkBgXEM9r4PiK7NfsJxbFq7UGk0oa3ibRSiBxzODgZIVlw==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.0.2.tgz",
+      "integrity": "sha512-KY4PZtQRf06sC2B1nKkm1hI2y7XFWqqA2lAmWRu12m7Zofc9aabLipEY8yijY7se0QMc4kDTVWp8d2uvbhyDFA==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -786,9 +786,9 @@
       }
     },
     "node_modules/@tiptap/extension-bold": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.0.1.tgz",
-      "integrity": "sha512-SYzs0e3FcAmJbYQkQ+LLRnoBNrFvG0i2zc+lFbrSYVCiEpqb5QPz5mgmQvnrH/Sl89WewnoeMu7pLqVEpfAWuw==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.0.2.tgz",
+      "integrity": "sha512-WRqc8WeKx3pmi0u0Svre5rhMeTT1c/Vch48BWTkUsmn2PAufg/mrmmR1fJ8Bp5soazEKknOT5LVe69OYKLfHIQ==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -798,9 +798,9 @@
       }
     },
     "node_modules/@tiptap/extension-bubble-menu": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.1.tgz",
-      "integrity": "sha512-ZA8T7+yjlHzEjBeOlWAqz/9XvBb/rJ7/PiYjM57UXuT/ZgPMPL7KXI/KtS7vaRCnmKaGL6EJ1tBcZhjU24vNzA==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.2.tgz",
+      "integrity": "sha512-cZDAMnf1+E711zY9RApWFajNp+ScRdN3L9+k6XEUnmTHlfVIeE1jsPmpH3PIZpMUpAO4TQ76DvLfEb4hKSu3eQ==",
       "dependencies": {
         "tippy.js": "^6.3.7"
       },
@@ -814,9 +814,9 @@
       }
     },
     "node_modules/@tiptap/extension-bullet-list": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.1.tgz",
-      "integrity": "sha512-IniXv9VgHkyWF2wJKxoILhNnJPwWNlIRW2LNSlXzkl70k0BsPGcAfiEIJtPqpVwh03QPc5v1y5UeuOOO6VQbqA==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.0.2.tgz",
+      "integrity": "sha512-n6P4N+3dZqBnyxpc6pz4qYHLOYr2oy6+K662GKzNQqe1TFSVr9+Vc/JmXiVPgwERs6RW6/Kdo7/9s454eDTIzg==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -826,9 +826,9 @@
       }
     },
     "node_modules/@tiptap/extension-code": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.0.1.tgz",
-      "integrity": "sha512-9Ygk2Ijfu89JFRwtId1x/u3yJUMfVSo6bkiUUmIFf04evgIgzBYMNl/KQerapmkcbkXWGCZ7/gcu+WUmU4c7lQ==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.0.2.tgz",
+      "integrity": "sha512-YG8BlqK50ZyYpBZ3KtO9/Ao+hQr3Z9wuW0fi7VIlwWferB20Px9WlBolAipRxfCh+oLmiacipHBIdGZGD+rC6A==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -838,9 +838,9 @@
       }
     },
     "node_modules/@tiptap/extension-code-block": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.0.1.tgz",
-      "integrity": "sha512-dPGKAlg0P2Qpikp8IF3r3kAD9II/Mc9cB8Wa3czwPr8r9Oobyy3UL7R7WqMy33abLgHmS/RuerOsTXz6sHBMww==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.0.2.tgz",
+      "integrity": "sha512-GL8ogok1tl1FkXwk0P0ZWYh6oAmSA+R3oubtDZJG1fLlezKLcLYCN/Q2jgYDHDwEOnxMc4JIiT7EYwJ0pqmNaQ==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -851,9 +851,9 @@
       }
     },
     "node_modules/@tiptap/extension-code-block-lowlight": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.0.1.tgz",
-      "integrity": "sha512-AUEeOfHSYh9a7+Lv57LSnYOJkC1lUhQC3AtjJK/1LG8NzDuL1XY+wpP0WKLKsH2qr95bJNi26amIcdVGtIVupg==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.0.2.tgz",
+      "integrity": "sha512-7BbRCKJE2oxsZ5n7HIjS0r/y1S/bSxEJgAFF1Tj3KN2IG3x48w+sqYxRMYmCZdoTexmmBpNF64uYXngKXB9/Ig==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -865,9 +865,9 @@
       }
     },
     "node_modules/@tiptap/extension-color": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-2.0.1.tgz",
-      "integrity": "sha512-NCfrwXvGjXbfHbm4DBjvJz1rXlWEXUtsXIXCVhRj/bUJHx/g1gV9Q5wcOLVWpd2h3y18H2YbO0LS1OypW8huYQ==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-2.0.2.tgz",
+      "integrity": "sha512-2TwXnGpMgbc1VYgUSWEEReR4Yy4K0rbdifO7RKy9xOuccT8+F5EccoWzb8R7ZMB4QVJ1cE9b3Pc0miPL43Tt3w==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -878,9 +878,9 @@
       }
     },
     "node_modules/@tiptap/extension-document": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.0.1.tgz",
-      "integrity": "sha512-9T14qBa6uKieJ+FCGm0nL9o8LTjwqlfc2pYQkkjXXWFivMXoVeL9rVIlarANgbJZKYsZ4L5tkyXcKQ32nZmMjg==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.0.2.tgz",
+      "integrity": "sha512-rY87m1sezlD37v5hGndiA/B/3upR3hQurSEsWhWyQE/11lOshPQKCCHfDV6KLwKdjd8lfwfbXueH/SBFHtrYAQ==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -890,9 +890,9 @@
       }
     },
     "node_modules/@tiptap/extension-dropcursor": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.0.1.tgz",
-      "integrity": "sha512-OuqMhj13hPSdxc6G3xDmGvwe3AfczMQjAoSPakNW9pSSzrjJJa3Tr08kI+TyZDUm3vKM4af+lL/oCeXSupGAbQ==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.0.2.tgz",
+      "integrity": "sha512-jHBq5fAlgUeZBHPtdAeS40Xmx+2sUvzTnHonU6cDKOpD3+dzwNHEqVSk/9vcjEIcqeM5w3eOIS/AGvoFq7RCjA==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -903,9 +903,9 @@
       }
     },
     "node_modules/@tiptap/extension-floating-menu": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.1.tgz",
-      "integrity": "sha512-gOf7FyromNyQpdaU/MWV99KMu6vypN+IlK8ViCBPvcM7w+e5eTh39wY/u5OquM7LsXd8KXLC2neq1XzQsAVmzw==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.2.tgz",
+      "integrity": "sha512-sYBBmMyjygHimtekZMzOAU1yvHjn36O1tZ+lXR/K2F97WWd5Y9WJo+iUrnbzBDMTXGvPi5ZTByXg36YKFRTmPg==",
       "dependencies": {
         "tippy.js": "^6.3.7"
       },
@@ -919,9 +919,9 @@
       }
     },
     "node_modules/@tiptap/extension-font-family": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-font-family/-/extension-font-family-2.0.1.tgz",
-      "integrity": "sha512-uUJippccCmy6qKCvzLJSphS79Cu1FghHLFQwc2NETmXAVNmyRcb8/61tF4hqDt8eEokvEdi8YNWD3JDm/wOBuw==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-font-family/-/extension-font-family-2.0.2.tgz",
+      "integrity": "sha512-uJ52qvSml2r8vtaZg4A2Cz7jx4+V5lT9cEKXsctH0uJJ4r5kMWOp0u7YB3AUGasOIpi/Mg++IK9dqt8zMadAlg==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -932,9 +932,9 @@
       }
     },
     "node_modules/@tiptap/extension-gapcursor": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.1.tgz",
-      "integrity": "sha512-NZbT4BIpXlf3gxqQMsZ173bJreGfr43eBwR/tJ8nBLcgBmFKiDfRTW3Whj7+EGD6Ek6rqUX0EekKG93Wes+I4w==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.2.tgz",
+      "integrity": "sha512-NimvDbM8Cc8+l/ZWJW8aqZRH6hzz1iJLOAMyj23UjHQWvKO0yqE1KBLGZI2GU+vizEK3LkZZXhXnh76rnTwgSQ==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -945,9 +945,9 @@
       }
     },
     "node_modules/@tiptap/extension-hard-break": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.0.1.tgz",
-      "integrity": "sha512-H8NdmOuYehuMMRd0iWOeyN8ukAebqu6BWbDiZUKm+yx+1b7woadVofojLnGlreQT2II3cC4g8D9J7JfrcQtbyg==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.0.2.tgz",
+      "integrity": "sha512-X/HG9Mzb4Qx/x67tkLK0JdG/fNL7IMP5gTpqOdgRT4m4vQA4ItuZHi4GlvWXfvtP6J0qfKKNimJvBQ5TZCZV1A==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -957,9 +957,9 @@
       }
     },
     "node_modules/@tiptap/extension-heading": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.0.1.tgz",
-      "integrity": "sha512-JkwlePSyDraVrWisr5lE/yqK4FXm5YSiiQiIo/1W9vFLdMPsAbRCSZrBO4fV7gyppbFgQQikEWDgxYHshq/TOg==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.0.2.tgz",
+      "integrity": "sha512-99PL9Rx8mioo13SNuIkhZm1VW6UuUKKG0b7fAimsunaEbbLNtToXIasS68pX8BkgWnsBfDhR0HyhnpIBRg7W0w==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -969,9 +969,9 @@
       }
     },
     "node_modules/@tiptap/extension-highlight": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-2.0.1.tgz",
-      "integrity": "sha512-k9rJnAGzjXWQSN3RqSVPQfsbtFAgZu2AnwSFH2VQ2Ea7oiAUOaD7Qcqrs0PLo/CxLEuWPAbULNngXUXZVseMBA==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-2.0.2.tgz",
+      "integrity": "sha512-FLW7VvHr6UjiHCxzGMrnTBCslDfUqpLJjg0j6P4+0ksKKEcjfgRNkI8ln5NB1IyphXYUvWczeD83rMTtX8+feA==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -981,9 +981,9 @@
       }
     },
     "node_modules/@tiptap/extension-history": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.0.1.tgz",
-      "integrity": "sha512-nT4XbZUEyi+xSnwIeG+JqfHmWhF+amCwYsr7oYM/oa/BFDGaRHaNIrS3D+DpzjRrKguuLDxyhZkJEu1K2Rjsqw==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.0.2.tgz",
+      "integrity": "sha512-K7u8HCEh9g35DSB4+c7z1wj9ais2cF3STDhyCRjBuLcCTgWVAim5cto4MkSdrXnLuWGgNK0jloWxVlMk3E1HPg==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -994,9 +994,9 @@
       }
     },
     "node_modules/@tiptap/extension-horizontal-rule": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.0.1.tgz",
-      "integrity": "sha512-JNgu0ioOBhn3Djn8vd41y3DrqeEfWMLIWNRVO81OqQRvWZhpRwIUuDoRrzR45wxjymq9flc+iz7zuFW1kszpQA==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.0.2.tgz",
+      "integrity": "sha512-QUqNnHqRXMZRE6uFRX0UDFn07JDiiNukPRCIL4vyV/YJ6HmQiBjbsS/1hLjAwBuqaN8dtlHLdR4PjMuA6qrUYw==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1007,9 +1007,9 @@
       }
     },
     "node_modules/@tiptap/extension-image": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.0.1.tgz",
-      "integrity": "sha512-FMXY0Y3nsDtAb1gNGidNhXnyJTZw6bOwYXg6jEV/K05HIz/svMQVkXLR04vSKczr7DWnlOmaJiqzge9Hc9WJ9Q==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.0.2.tgz",
+      "integrity": "sha512-Jyfzp78Mn20R1Bcpz4Cm47EOU5xrOcDkhSisvNcFqcaxSV/CHyNVPDRpRMussK3k7qDcDuf2flDjrTRXPWZqIg==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1019,9 +1019,9 @@
       }
     },
     "node_modules/@tiptap/extension-italic": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.0.1.tgz",
-      "integrity": "sha512-lVIyKa3qjqD4rUKSzVghkS5xonJ8k+msvy5Sr5xd6duwEBOk7e0oKtXmdn7B1VBH64i/U3Cx0QRgDlEyukvV2Q==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.0.2.tgz",
+      "integrity": "sha512-AK8m4JKvjv2LgTho2GLPW4UI9Z8Oc1tErG9TAeJzud8NFFZ2iOOueAwyGjPLaEQxWaE2p5xlKjbBUWDV5bd6AQ==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1031,9 +1031,9 @@
       }
     },
     "node_modules/@tiptap/extension-list-item": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.0.1.tgz",
-      "integrity": "sha512-WxLRcwhMGp12+hV1nLTRVxPJ3ZsjLzA3tyaYySBf6IQpmoikH8DbzvprwV30lHfYbZvcQWQJx3ECIcQdGbtPPg==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.0.2.tgz",
+      "integrity": "sha512-q8nz79iB6EvkBucm0OwUg7SX3vDGCRttmf0OrEBsoUCoFt/NhrejUCMDSbysHtYp+EtZcKu4VsPuNHU3C7F1vQ==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1043,9 +1043,9 @@
       }
     },
     "node_modules/@tiptap/extension-mention": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-mention/-/extension-mention-2.0.1.tgz",
-      "integrity": "sha512-Wb7uX+MgjxMvhaaO6oTRcMBkhmsA7Rd5cERe4Yc2jkqIZKoyoL91AL5qqFJxVEhn18bXsqxbaCBpcjLxjYNCIQ==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-mention/-/extension-mention-2.0.2.tgz",
+      "integrity": "sha512-5nUSlqYi5x+I5Q84+l8OL+4R/NRAN0wBrEN0Cps4GJB3dqINMsTbjSbIwRk8bw79XBHNJI1PndBjVktBgwYxNA==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1057,9 +1057,9 @@
       }
     },
     "node_modules/@tiptap/extension-ordered-list": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.1.tgz",
-      "integrity": "sha512-7Q3hqAy2TYZ1p3J0GtEDcUJrbC/NxzWNF1H4zKW3VQGQhffUDNgHBk4uCLLVyf1A3vm50JmEAwQFrrwoozFmqQ==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.0.2.tgz",
+      "integrity": "sha512-fgWIQNDlC2pYNnS9Iqw2UZaNIInfks3mZm0Aj9DQ78LbTYDAcGvHcWaulLkd0CcfD6smTYwi9oBzP3dpPbcPTw==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1069,9 +1069,9 @@
       }
     },
     "node_modules/@tiptap/extension-paragraph": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.0.1.tgz",
-      "integrity": "sha512-fVr5BZ7glyf/80vIO+GSQdeIcTvuXKGW9QvU0Mw8y1ek9Edd3MLWMxKdDurTswApnxIluvJFjmzp4XEogHHlhg==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.0.2.tgz",
+      "integrity": "sha512-BuiUV8Wh8DjRrNmmk5sIlVlk0V8P4BT2fCB4H7Ar+QDUPKY1g0djB/5g3F70iZh1Z7CiY1ctoL8VQznvtb0KsQ==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1081,9 +1081,9 @@
       }
     },
     "node_modules/@tiptap/extension-placeholder": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.0.1.tgz",
-      "integrity": "sha512-Jc0SrZw6HQ6Ddxr58SAAw5QPYh2pgjc1OSfrKn7+zBzcZDMFLWWgx9lTRwlR5L5VqTEuWqatJCfDrgjbnE4hLw==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-2.0.2.tgz",
+      "integrity": "sha512-5D46ONEN4Hcgn9xwWgY0mSUp9DAM/z74P9vZcdChUXxj96L2ngM/nU92qEby0kGCSGoO3ucxJGf/aL8KhGqIxQ==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1094,9 +1094,9 @@
       }
     },
     "node_modules/@tiptap/extension-strike": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.0.1.tgz",
-      "integrity": "sha512-gHNO47T5tbeFzhjJUn2Ob5RrqkG4joVyvUv3fFkSqpNngTIcqT5hJ7A3tDj2CKQGmUpU5SxM68sHMoIGpXdWIQ==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.0.2.tgz",
+      "integrity": "sha512-v5goRcfORMkHaA4/mQH80mO0IVFJup1sSrSd4lNUwWN68Qy2ckMVRLRgvbk0JjG2kbamFgJ4LmN/3ceZvUj27A==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1106,9 +1106,9 @@
       }
     },
     "node_modules/@tiptap/extension-table": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-2.0.1.tgz",
-      "integrity": "sha512-HpFuDrBolIC9E2KGZ4PAEncgX+Kk3kcCccG/Cuiigb3M/VnJm6l5m5UEOa9egSVW2wmdj4xsM9adfHNcsWi6Mw==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-2.0.2.tgz",
+      "integrity": "sha512-0HUpCQ+roTZqgMDtKQfgq8Yjo5SSxUJc+lMVHqcdeW2UwPkwXfAPCZ0ojle8SyQgQjv6GTcVTssvzq7+HGpd0Q==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1119,9 +1119,9 @@
       }
     },
     "node_modules/@tiptap/extension-table-cell": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-2.0.1.tgz",
-      "integrity": "sha512-ymxDC5leH/e7/V1JAup478nt3ellxMxJ9F+q/y3O/mN0wkisZXGWue2GYPMx+ymrKyF1K0pS2I0AYqxQ6HzMPw==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-2.0.2.tgz",
+      "integrity": "sha512-tz4H+GPhV/Ed7DG4GXChlq1+sbWwBNb1v9Lp0D1JiU3VKzO9ZbOiPg0YDn+m3cE2rhpVXNKvZzlAG0IALl0VEQ==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1131,9 +1131,9 @@
       }
     },
     "node_modules/@tiptap/extension-table-header": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-2.0.1.tgz",
-      "integrity": "sha512-QBpoIZQ7skaqQZE62xjwxtaVuJb42g7w4sqLYLwH2ChYBurpsbBp8fb2V/8GtuTsJNdXNmsYmZAkJw0KkHk4jA==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-2.0.2.tgz",
+      "integrity": "sha512-v0diLuGQ200uZ1cl+xkFpRozLtuMpAFd/G2JFsi2o2bRNh2U7y5F48LOCIoeOOr5vqDy4qD04feUVWv/qdYMjg==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1143,9 +1143,9 @@
       }
     },
     "node_modules/@tiptap/extension-table-row": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-2.0.1.tgz",
-      "integrity": "sha512-Asa7f6no2z+yGZ7BVmvBLEMFCs5oW2c4a6aMuoG/qHQzuIRfI7y433Pe+SUJRhWXNLNLJLhM42g+fBgzFABo0g==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-2.0.2.tgz",
+      "integrity": "sha512-R5mI/S9iMNVqvdUVHEOC96C55vmIkvYEO7UQUijXgsjPHufoKQr+i2EH85BbmzHL+JdF9S5MMShczQJzJuAAkA==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1155,9 +1155,9 @@
       }
     },
     "node_modules/@tiptap/extension-task-item": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-2.0.1.tgz",
-      "integrity": "sha512-JKiXxvBJqn4d6vdJ2ejDFXExxGPwlAeaj8uQNzUMI93v5pkqyeU22lYMoMBK0EPeBy7EhMiIEcxY8tonNFT73g==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-task-item/-/extension-task-item-2.0.2.tgz",
+      "integrity": "sha512-YGzQuLXC4HQOHcvGw2MArUVDxEW2LX7IV3qeS89+H2HWZQDoWOoGRMHBZoRRAgzppG36Y5rQVWae/avDc5RuOQ==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1168,9 +1168,9 @@
       }
     },
     "node_modules/@tiptap/extension-task-list": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-2.0.1.tgz",
-      "integrity": "sha512-jGP94bR2FyvZwOE2f6SmW7H6RBfhMkwc+HaFwYHadECbDbmXgIzITI85T4qWm481PhXKOEK0i2OTcJiEjU5P5Q==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-task-list/-/extension-task-list-2.0.2.tgz",
+      "integrity": "sha512-lxrRYNKwX1P/Q8HmbtL+IqoMwr4A4MSWUKdXFKde/bbXfFczeOopfW9ioaIvvMiXYIoDz8x3/QpHRxt8t11pCA==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1180,9 +1180,9 @@
       }
     },
     "node_modules/@tiptap/extension-text": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.0.1.tgz",
-      "integrity": "sha512-jwfpf71JZn04T4xcOMGJXCHTZoyvVtBusn6ZyRSQT4cVnDUgWcIbmIW5xccvGy8klBepBDS2ff+Nce3pxQnqug==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.0.2.tgz",
+      "integrity": "sha512-kAO+WurWOyHIV/x8qHMF3bSlWrdlPtjEYmf+w8wHKy3FzE55eF6SsGt4FymClNkJmyXdgflXBB3Wv/Z53myy8g==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1192,9 +1192,9 @@
       }
     },
     "node_modules/@tiptap/extension-text-align": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-2.0.1.tgz",
-      "integrity": "sha512-XhMcx7qaReJlB5BeaXFQ+8QZED45/BRwCFqE+WkfwvWH1M/ySiiazOJyootDTudx0lijbf2rUCUKYc4oexoMew==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-2.0.2.tgz",
+      "integrity": "sha512-cNYOQPpqAOquTjSm2lSzP9J1d5T9FueWArq2A3FsYzOSZfVAJmqp9gn/bfCKveKgE5m7nLN3g3o+RNryIWTr2w==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1204,9 +1204,9 @@
       }
     },
     "node_modules/@tiptap/extension-text-style": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.0.1.tgz",
-      "integrity": "sha512-fAS3zGsSW2xOuozmpKiRzOkC031GLK+V3yXWOOMWPOKwx1YoA7Q3RFELXgZGIO1lHJnwcrKZBCcu+4VAxmaNug==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.0.2.tgz",
+      "integrity": "sha512-0XJJA4MKBi/jOfN60LC8beSzjx0BMWFhZqtFczfOq73YTbQriDOkKFLn3AiEc8oRlYNdRnIg4chVPCheBkoKwg==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1216,9 +1216,9 @@
       }
     },
     "node_modules/@tiptap/extension-typography": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/extension-typography/-/extension-typography-2.0.1.tgz",
-      "integrity": "sha512-0yyM9YjNx71zmb8EYIVIxU0C2zi5IDe3FzCDEdqMzi6bwZZzrtqYeX/fG1IW974RVWKGu+ScrNDd5Y5lcUriOw==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/extension-typography/-/extension-typography-2.0.2.tgz",
+      "integrity": "sha512-EopmjSP9ZKM98vUa+W29L5SxfQgBOgJTZJ6oR3jvQ15Ctu0pssOAI+7u7U5OnzUaaiR3w4M6a9iDAwCjSmwNiA==",
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/ueberdosis"
@@ -1228,9 +1228,9 @@
       }
     },
     "node_modules/@tiptap/pm": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.0.1.tgz",
-      "integrity": "sha512-Vu6PAVNbIArhNCA3TXC9bX4Qj4duI+3jZhGptTDU+4wLHSjp6p1zM+RKRHFdIIBZG+v9uATWyitrvNd7qHlJdQ==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.0.2.tgz",
+      "integrity": "sha512-vXlI82bZ4XrmVD6m/pO27gqlm+tU57mpjy9WjkJpEUOifQZK8LihR3l5k55Z0RqalV4/E79iU1cp8mw0v13nhA==",
       "dependencies": {
         "prosemirror-changeset": "^2.2.0",
         "prosemirror-collab": "^1.3.0",
@@ -1260,29 +1260,29 @@
       }
     },
     "node_modules/@tiptap/starter-kit": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.0.1.tgz",
-      "integrity": "sha512-LxSAwVpaLsbUodDQRvfAiWc6QYG2Fq1m5ZDzoPBAFJq/aCcI0Gt5Gf3AQrOWpuj9FV8kelbr9gUkPqJR60K+Pg==",
-      "dependencies": {
-        "@tiptap/core": "^2.0.1",
-        "@tiptap/extension-blockquote": "^2.0.1",
-        "@tiptap/extension-bold": "^2.0.1",
-        "@tiptap/extension-bullet-list": "^2.0.1",
-        "@tiptap/extension-code": "^2.0.1",
-        "@tiptap/extension-code-block": "^2.0.1",
-        "@tiptap/extension-document": "^2.0.1",
-        "@tiptap/extension-dropcursor": "^2.0.1",
-        "@tiptap/extension-gapcursor": "^2.0.1",
-        "@tiptap/extension-hard-break": "^2.0.1",
-        "@tiptap/extension-heading": "^2.0.1",
-        "@tiptap/extension-history": "^2.0.1",
-        "@tiptap/extension-horizontal-rule": "^2.0.1",
-        "@tiptap/extension-italic": "^2.0.1",
-        "@tiptap/extension-list-item": "^2.0.1",
-        "@tiptap/extension-ordered-list": "^2.0.1",
-        "@tiptap/extension-paragraph": "^2.0.1",
-        "@tiptap/extension-strike": "^2.0.1",
-        "@tiptap/extension-text": "^2.0.1"
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.0.2.tgz",
+      "integrity": "sha512-s0yE6nEVYnxnHHdLxZ6McMhOAMbZ1czAj9qvmc1Mra0U/p08fg3VTn1gIgy8FA3t0TWUNpuwsO4FcgKlWaQ+eg==",
+      "dependencies": {
+        "@tiptap/core": "^2.0.2",
+        "@tiptap/extension-blockquote": "^2.0.2",
+        "@tiptap/extension-bold": "^2.0.2",
+        "@tiptap/extension-bullet-list": "^2.0.2",
+        "@tiptap/extension-code": "^2.0.2",
+        "@tiptap/extension-code-block": "^2.0.2",
+        "@tiptap/extension-document": "^2.0.2",
+        "@tiptap/extension-dropcursor": "^2.0.2",
+        "@tiptap/extension-gapcursor": "^2.0.2",
+        "@tiptap/extension-hard-break": "^2.0.2",
+        "@tiptap/extension-heading": "^2.0.2",
+        "@tiptap/extension-history": "^2.0.2",
+        "@tiptap/extension-horizontal-rule": "^2.0.2",
+        "@tiptap/extension-italic": "^2.0.2",
+        "@tiptap/extension-list-item": "^2.0.2",
+        "@tiptap/extension-ordered-list": "^2.0.2",
+        "@tiptap/extension-paragraph": "^2.0.2",
+        "@tiptap/extension-strike": "^2.0.2",
+        "@tiptap/extension-text": "^2.0.2"
       },
       "funding": {
         "type": "github",
@@ -1304,12 +1304,12 @@
       }
     },
     "node_modules/@tiptap/vue-3": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-2.0.1.tgz",
-      "integrity": "sha512-4szBIsZZvnnRv6G7MoQ4nFIYwAvcOSbj+FsR/H666VK+sU0zwaD3opqN/5c2kYMeeCmmU/UuMbnQ9slQEnETjA==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/@tiptap/vue-3/-/vue-3-2.0.2.tgz",
+      "integrity": "sha512-Z0Znj1a+4AW58749PvEPi/YjSY3U4Pu28g7jjD6rGcae0DCa0g8TQuPsPqgxBsZkFJ0JJisasEAKJRX96zlUbg==",
       "dependencies": {
-        "@tiptap/extension-bubble-menu": "^2.0.1",
-        "@tiptap/extension-floating-menu": "^2.0.1"
+        "@tiptap/extension-bubble-menu": "^2.0.2",
+        "@tiptap/extension-floating-menu": "^2.0.2"
       },
       "funding": {
         "type": "github",
@@ -6433,9 +6433,9 @@
       }
     },
     "node_modules/quasar": {
-      "version": "2.11.9",
-      "resolved": "https://registry.npmjs.org/quasar/-/quasar-2.11.9.tgz",
-      "integrity": "sha512-ZHSZRQJk/zN6whhvihh0EyRpAs3IYBZZ+1O5/RSe7nXyOXOVi6RwPBzleab5HoTxtHL5Yuk3/bKgb3CywAyBQQ==",
+      "version": "2.11.10",
+      "resolved": "https://registry.npmjs.org/quasar/-/quasar-2.11.10.tgz",
+      "integrity": "sha512-pV7bMdY/FUmOvNhZ2XjKSXJH92fsDu0cU/z7a9roPKV54cW41N1en3sLATrirjPComyZnk4uXrMdGtXp8+IpCg==",
       "engines": {
         "node": ">= 10.18.1",
         "npm": ">= 6.13.4",

+ 30 - 28
ux/package.json

@@ -8,38 +8,40 @@
   "scripts": {
     "dev": "quasar dev",
     "build": "quasar build",
-    "lint": "eslint --ext .js,.vue ./"
+    "lint": "eslint --ext .js,.vue ./",
+    "ncu": "ncu -x codemirror,codemirror-asciidoc",
+    "ncu-u": "ncu -u -x codemirror,codemirror-asciidoc"
   },
   "dependencies": {
     "@apollo/client": "3.7.11",
     "@lezer/common": "1.0.2",
     "@mdi/font": "7.2.96",
-    "@quasar/extras": "1.16.1",
-    "@tiptap/core": "2.0.1",
-    "@tiptap/extension-code-block": "2.0.1",
-    "@tiptap/extension-code-block-lowlight": "2.0.1",
-    "@tiptap/extension-color": "2.0.1",
-    "@tiptap/extension-dropcursor": "2.0.1",
-    "@tiptap/extension-font-family": "2.0.1",
-    "@tiptap/extension-gapcursor": "2.0.1",
-    "@tiptap/extension-hard-break": "2.0.1",
-    "@tiptap/extension-highlight": "2.0.1",
-    "@tiptap/extension-history": "2.0.1",
-    "@tiptap/extension-image": "2.0.1",
-    "@tiptap/extension-mention": "2.0.1",
-    "@tiptap/extension-placeholder": "2.0.1",
-    "@tiptap/extension-table": "2.0.1",
-    "@tiptap/extension-table-cell": "2.0.1",
-    "@tiptap/extension-table-header": "2.0.1",
-    "@tiptap/extension-table-row": "2.0.1",
-    "@tiptap/extension-task-item": "2.0.1",
-    "@tiptap/extension-task-list": "2.0.1",
-    "@tiptap/extension-text-align": "2.0.1",
-    "@tiptap/extension-text-style": "2.0.1",
-    "@tiptap/extension-typography": "2.0.1",
-    "@tiptap/pm": "2.0.1",
-    "@tiptap/starter-kit": "2.0.1",
-    "@tiptap/vue-3": "2.0.1",
+    "@quasar/extras": "1.16.2",
+    "@tiptap/core": "2.0.2",
+    "@tiptap/extension-code-block": "2.0.2",
+    "@tiptap/extension-code-block-lowlight": "2.0.2",
+    "@tiptap/extension-color": "2.0.2",
+    "@tiptap/extension-dropcursor": "2.0.2",
+    "@tiptap/extension-font-family": "2.0.2",
+    "@tiptap/extension-gapcursor": "2.0.2",
+    "@tiptap/extension-hard-break": "2.0.2",
+    "@tiptap/extension-highlight": "2.0.2",
+    "@tiptap/extension-history": "2.0.2",
+    "@tiptap/extension-image": "2.0.2",
+    "@tiptap/extension-mention": "2.0.2",
+    "@tiptap/extension-placeholder": "2.0.2",
+    "@tiptap/extension-table": "2.0.2",
+    "@tiptap/extension-table-cell": "2.0.2",
+    "@tiptap/extension-table-header": "2.0.2",
+    "@tiptap/extension-table-row": "2.0.2",
+    "@tiptap/extension-task-item": "2.0.2",
+    "@tiptap/extension-task-list": "2.0.2",
+    "@tiptap/extension-text-align": "2.0.2",
+    "@tiptap/extension-text-style": "2.0.2",
+    "@tiptap/extension-typography": "2.0.2",
+    "@tiptap/pm": "2.0.2",
+    "@tiptap/starter-kit": "2.0.2",
+    "@tiptap/vue-3": "2.0.2",
     "apollo-upload-client": "17.0.0",
     "browser-fs-access": "0.33.0",
     "clipboard": "2.0.11",
@@ -81,7 +83,7 @@
     "prosemirror-transform": "1.7.1",
     "prosemirror-view": "1.30.2",
     "pug": "3.0.2",
-    "quasar": "2.11.9",
+    "quasar": "2.11.10",
     "slugify": "1.6.6",
     "socket.io-client": "4.6.1",
     "tabulator-tables": "5.4.4",

部分文件因为文件数量过多而无法显示