web.mjs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import bodyParser from 'body-parser'
  2. import compression from 'compression'
  3. import cookieParser from 'cookie-parser'
  4. import cors from 'cors'
  5. import express from 'express'
  6. import session from 'express-session'
  7. import KnexSessionStore from 'connect-session-knex'
  8. import favicon from 'serve-favicon'
  9. import path from 'node:path'
  10. import { set } from 'lodash-es'
  11. import auth from './core/auth.mjs'
  12. import mail from './core/mail.mjs'
  13. import system from './core/system.mjs'
  14. import ctrlAuth from './controllers/auth.mjs'
  15. import ctrlCommon from './controllers/common.mjs'
  16. import ctrlSsl from './controllers/ssl.mjs'
  17. import ctrlWs from './controllers/ws.mjs'
  18. export async function init () {
  19. // ----------------------------------------
  20. // Load core modules
  21. // ----------------------------------------
  22. WIKI.auth = auth.init()
  23. WIKI.mail = mail.init()
  24. WIKI.system = system.init()
  25. // ----------------------------------------
  26. // Define Express App
  27. // ----------------------------------------
  28. const app = express()
  29. WIKI.app = app
  30. app.use(compression())
  31. // ----------------------------------------
  32. // Initialize HTTP/HTTPS Server
  33. // ----------------------------------------
  34. const useHTTPS = WIKI.config.ssl.enabled === true || WIKI.config.ssl.enabled === 'true' || WIKI.config.ssl.enabled === 1 || WIKI.config.ssl.enabled === '1'
  35. await WIKI.servers.initHTTP()
  36. if (useHTTPS) {
  37. await WIKI.servers.initHTTPS()
  38. }
  39. await WIKI.servers.initWebSocket()
  40. // ----------------------------------------
  41. // Attach WebSocket Server
  42. // ----------------------------------------
  43. ctrlWs()
  44. // ----------------------------------------
  45. // Security
  46. // ----------------------------------------
  47. app.use((req, res, next) => {
  48. // -> Disable X-Powered-By
  49. req.app.disable('x-powered-by')
  50. // -> Disable Frame Embedding
  51. if (WIKI.config.security.securityIframe) {
  52. res.set('X-Frame-Options', 'deny')
  53. }
  54. // -> Re-enable XSS Fitler if disabled
  55. res.set('X-XSS-Protection', '1; mode=block')
  56. // -> Disable MIME-sniffing
  57. res.set('X-Content-Type-Options', 'nosniff')
  58. // -> Disable IE Compatibility Mode
  59. res.set('X-UA-Compatible', 'IE=edge')
  60. // -> Disables referrer header when navigating to a different origin
  61. if (WIKI.config.security.securityReferrerPolicy) {
  62. res.set('Referrer-Policy', 'same-origin')
  63. }
  64. // -> Enforce HSTS
  65. if (WIKI.config.security.securityHSTS) {
  66. res.set('Strict-Transport-Security', `max-age=${WIKI.config.security.securityHSTSDuration}; includeSubDomains`)
  67. }
  68. // -> Prevent Open Redirect from user provided URL
  69. if (WIKI.config.security.securityOpenRedirect) {
  70. // Strips out all repeating / character in the provided URL
  71. req.url = req.url.replace(/(\/)(?=\/*\1)/g, '')
  72. }
  73. next()
  74. })
  75. app.use(cors({ origin: false }))
  76. app.options('*', cors({ origin: false }))
  77. if (WIKI.config.security.securityTrustProxy) {
  78. app.enable('trust proxy')
  79. }
  80. // ----------------------------------------
  81. // Public Assets
  82. // ----------------------------------------
  83. app.use(favicon(path.join(WIKI.ROOTPATH, 'assets', 'favicon.ico')))
  84. app.use('/_assets', express.static(path.join(WIKI.ROOTPATH, 'assets/_assets'), {
  85. index: false,
  86. maxAge: '7d'
  87. }))
  88. app.use('/_assets/svg/twemoji', async (req, res, next) => {
  89. try {
  90. WIKI.asar.serve('twemoji', req, res, next)
  91. } catch (err) {
  92. res.sendStatus(404)
  93. }
  94. })
  95. // ----------------------------------------
  96. // Blocks
  97. // ----------------------------------------
  98. app.use('/_blocks', express.static(path.join(WIKI.ROOTPATH, 'blocks/dist'), {
  99. index: false,
  100. maxAge: '7d'
  101. }))
  102. // ----------------------------------------
  103. // SSL Handlers
  104. // ----------------------------------------
  105. app.use('/', ctrlSsl())
  106. // ----------------------------------------
  107. // Passport Authentication
  108. // ----------------------------------------
  109. app.use(cookieParser())
  110. app.use(session({
  111. secret: WIKI.config.auth.secret,
  112. resave: false,
  113. saveUninitialized: false,
  114. store: new KnexSessionStore(session)({
  115. knex: WIKI.db.knex
  116. })
  117. }))
  118. app.use(WIKI.auth.passport.initialize())
  119. app.use(WIKI.auth.authenticate)
  120. // ----------------------------------------
  121. // GraphQL Server
  122. // ----------------------------------------
  123. app.use(bodyParser.json({ limit: WIKI.config.bodyParserLimit || '1mb' }))
  124. await WIKI.servers.startGraphQL()
  125. // ----------------------------------------
  126. // SEO
  127. // ----------------------------------------
  128. app.use((req, res, next) => {
  129. if (req.path.length > 1 && req.path.endsWith('/')) {
  130. let query = req.url.slice(req.path.length) || ''
  131. res.redirect(301, req.path.slice(0, -1) + query)
  132. } else {
  133. set(res.locals, 'pageMeta.url', `${WIKI.config.host}${req.path}`)
  134. next()
  135. }
  136. })
  137. // ----------------------------------------
  138. // View Engine Setup
  139. // ----------------------------------------
  140. app.set('views', path.join(WIKI.SERVERPATH, 'views'))
  141. app.set('view engine', 'pug')
  142. app.use(bodyParser.urlencoded({ extended: false, limit: '1mb' }))
  143. // ----------------------------------------
  144. // View accessible data
  145. // ----------------------------------------
  146. app.locals.analyticsCode = {}
  147. app.locals.basedir = WIKI.ROOTPATH
  148. app.locals.config = WIKI.config
  149. app.locals.pageMeta = {
  150. title: '',
  151. description: WIKI.config.description,
  152. image: '',
  153. url: '/'
  154. }
  155. app.locals.devMode = WIKI.devMode
  156. // ----------------------------------------
  157. // HMR (Dev Mode Only)
  158. // ----------------------------------------
  159. if (global.DEV) {
  160. app.use(global.WP_DEV.devMiddleware)
  161. app.use(global.WP_DEV.hotMiddleware)
  162. }
  163. // ----------------------------------------
  164. // Routing
  165. // ----------------------------------------
  166. app.use(async (req, res, next) => {
  167. const currentSite = await WIKI.db.sites.getSiteByHostname({ hostname: req.hostname })
  168. if (!currentSite) {
  169. return res.status(404).send('Site Not Found')
  170. }
  171. res.locals.siteConfig = {
  172. id: currentSite.id,
  173. title: currentSite.config.title,
  174. darkMode: currentSite.config.theme.dark,
  175. lang: currentSite.config.locales.primary,
  176. rtl: false, // TODO: handle RTL
  177. company: currentSite.config.company,
  178. contentLicense: currentSite.config.contentLicense
  179. }
  180. res.locals.theming = {
  181. }
  182. res.locals.langs = await WIKI.db.locales.getNavLocales({ cache: true })
  183. res.locals.analyticsCode = await WIKI.db.analytics.getCode({ cache: true })
  184. next()
  185. })
  186. app.use('/', ctrlAuth())
  187. app.use('/', ctrlCommon())
  188. // ----------------------------------------
  189. // Error handling
  190. // ----------------------------------------
  191. app.use((req, res, next) => {
  192. const err = new Error('Not Found')
  193. err.status = 404
  194. next(err)
  195. })
  196. app.use((err, req, res, next) => {
  197. if (req.path === '/_graphql') {
  198. res.status(err.status || 500).json({
  199. data: {},
  200. errors: [{
  201. message: err.message,
  202. path: []
  203. }]
  204. })
  205. } else {
  206. res.status(err.status || 500)
  207. set(res.locals, 'pageMeta.title', 'Error')
  208. res.render('error', {
  209. message: err.message,
  210. error: WIKI.IS_DEBUG ? err : {}
  211. })
  212. }
  213. })
  214. // ----------------------------------------
  215. // Start HTTP Server(s)
  216. // ----------------------------------------
  217. await WIKI.servers.startHTTP()
  218. if (useHTTPS) {
  219. await WIKI.servers.startHTTPS()
  220. }
  221. return true
  222. }