web.mjs 7.3 KB

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